0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI駆動開発で業務進捗管理アプリを作った話

0
Last updated at Posted at 2026-02-28

はじめに

暇なので作りました
コードは全部Geminiに投げました

技術スタック

  • Python 3.12
  • CustomTkinter
  • JSON / CSV (utf-8-sig)
  • PyInstaller (--onefile --noconsole)

動作画面

管理者画面

  1. 進捗管理パネル.exeを実行
  2. 初回のみ、config.jsonを参照または作成する必要がある
  3. 管理者パネルが表示される
    image.png
パネルボタン説明
  • リアルタイム更新はon/offが可能
    • 間隔はmsで管理可能(初期設定は3000ms)
  • ログ出力フォルダは管理者パネルで設定可能
  • 表示名を管理者側で管理可能
    • 初期設定はPC名を割り当てるため
  • タスクリストはcsvで追加/削除
  • 共有設定を保存
    • json形式で出力する※後述
  • 全ログデータを集計出力
  • 接続先変更ボタンで参照config.jsonを変更

ユーザー利用画面

  1. 進捗管理くん.exeを実行
  2. 初回のみ、config.jsonを参照する必要がある
  3. 画面が表示される
    image.png
    ※個人PC名部分を塗りつぶしています
パネルボタン説明
  • 画面真ん中コンボボックスでタスクを設定
    • 管理画面で設定したタスクリストを参照する
  • 休憩入り, 業務終了はボタンを押すだけ
    • 休憩は稼働時間がカウントされる
      • 工数計算用
  • 設定ファイルを再選択ボタンで参照config.jsonを変更

設計について

技術に詳しくない人が、劣悪な環境(共有フォルダ運用・低スペックPC)でも確実に動かせること

  1. 設定ファイルの配布フェーズの簡略化
  2. SSOT (Single Source of Truth) の徹底
  3. 「休憩」「終了」 工数の導入
  4. Excelファーストの設計
  5. 低スペックPCでも動くように調整

設定ファイルの配布フェーズの簡略化

通常、クライアント・サーバー型の構成にしない場合、各PCに設定ファイルを配る手間が発生
これが導入・運用の最大の壁になる

初期設定ウィザード方式で対処
アプリ側に「設定ファイルのパスを記憶する(admin_settings.json / user_settings.json)」機能を持たせた
→管理者は共有フォルダにconfig.jsonを1つ置くだけでOK
ユーザーは初回起動時にそのファイルを選ぶだけで、自分専用の接続設定がローカルに保存される


  • 接続先パスの動的解決
    起動時に「自分の足元にパスの記憶があるか」をチェックし、なければファイルダイアログでユーザーに指定させる
test.py
import os
import json
from tkinter import filedialog, messagebox

# ローカルに保存される「接続先記憶ファイル」
LOCAL_MEMO_FILE = "user_settings.json" 

def get_shared_config():
    # 1. ローカルの記憶を確認
    if os.path.exists(LOCAL_MEMO_FILE):
        with open(LOCAL_MEMO_FILE, "r", encoding="utf-8") as f:
            config_path = json.load(f).get("config_path")
            
            # 記憶されたパスが有効ならそれを返す
            if config_path and os.path.exists(config_path):
                with open(config_path, "r", encoding="utf-8") as cf:
                    return json.load(cf), config_path

    # 2. 記憶がない、またはパスが無効ならユーザーに場所を聞く
    messagebox.showinfo("初期設定", "共有フォルダ内の config.json を選択してください。")
    path = filedialog.askopenfilename(title="config.jsonを選択", filetypes=[("JSON", "*.json")])
    
    if path:
        # 選んでもらったパスを次回の起動のためにローカルへ保存
        with open(LOCAL_MEMO_FILE, "w", encoding="utf-8") as f:
            json.dump({"config_path": path}, f)
            
        with open(path, "r", encoding="utf-8") as cf:
            return json.load(cf), path
            
    return None, None
  • 管理者側で最初のconfig.jsonが存在しない場合のデッドロック回避
    管理者は「最初のconfig.json」を作る役割も兼ねるため、既存ファイルを選ぶ(askopenfilename)だけでなく、新しく作る(asksaveasfilename)処理を分岐させ、デッドロックを回避
test.py
# 管理者パネル起動時の分岐ロジック(抜粋)
if not config_exists:
    # 既存接続か新規作成かをユーザーに問う
    is_connect = messagebox.askyesno("初期設定", "既存のconfig.jsonに接続しますか?\n(いいえを選ぶと新規作成します)")
    
    if is_connect:
        path = filedialog.askopenfilename(...) # 上書き警告が出ない
    else:
        path = filedialog.asksaveasfilename(...) # 新規ファイルを作成

SSOT (Single Source of Truth) の徹底

前項で説明した通り、管理者・利用者ともに、すべて1つのconfig.jsonを見に行く。

  • マッピング管理: PC名と表示名(個人名)の紐付けを管理者が一括で設定可能。これにより、ユーザーに自分の名前を入力してと頼む必要がなくなる
  • デッドロックの回避: 管理者パネルはconfig.json がなければその場で生成。これが最初の1個となり、システム全体が動き出す起点になる

  • ID管理の外部化(PC名 → 表示名への解決)
    ユーザーアプリは「自分が誰か」を知る必要はありません。ただPC名(Hostname)をログに刻むだけ
    表示名への変換は、管理者が一元管理するconfig.json内のマッピングリストによって「後付け」で解決
test.py
# 管理者パネル:共有設定からPC名を表示名に変換するコア処理
def get_user_display_name(self, pc_name):
    """
    config.jsonに記述されたマッピングを参照し、
    未登録のPCはそのままの名前を、登録済みなら表示名を返す。
    """
    mapping = self.config.get("user_mapping", {})
    return mapping.get(pc_name, pc_name)
  • 管理者による「動的な状態監視」
    全ユーザーの進捗ログが1つのフォルダに集約され、管理者はそのファイルを読み取り専用で叩き続けることで、DBなしでリアルタイム監視を実現
test.py
# 管理者パネル:共有フォルダの全ファイルを走査し、最新の状態をマッピング
def refresh_monitor(self):
    log_path = self.config["save_path"]
    for f in glob.glob(os.path.join(log_path, "*.csv")):
        pc_name = os.path.basename(f).replace(".csv", "")
        # 表示名の解決(SSOT参照)
        display_name = self.get_user_display_name(pc_name)
        
        with open(f, "r", encoding="utf-8-sig") as csv_f:
            rows = list(csv.reader(csv_f))
            if len(rows) > 1:
                last_row = rows[-1]
                # 終了時間が空 = 「現在進行中」と定義(ビジネスロジックの共有)
                if not last_row[2]:
                    # ここでTreeview(画面表示)を更新
                    self.tree.insert("", "end", values=(display_name, last_row[0], ...))

「休憩」「終了」 工数の導入

主に休憩時間という工数を管理したい現場向け

  • 休憩入り: 現在のタスクを締め、自動で「休憩」タスクを開始。工数として時間をカウントし続ける
  • 業務終了: その瞬間にすべての記録をストップ。翌朝まで「休憩中」や「作業中」が残ってしまう事故を防ぐ

  • 共通処理:現在タスクの封印
    どんな遷移をするにしても、「今やっていること」を確定させる処理を共通化
test.py
def stop_current_task(self):
    """
    現在稼働中のタスクがあれば、その瞬間の時刻を『終了時間』として
    CSVの3列目に打刻して1行を完結させる。
    """
    if self.current_task and self.start_time:
        end_time = datetime.now()
        # 既存の開始行に対して、終了時間を追記して保存
        self.save_log(self.current_task, self.start_time, end_time)
  • 「休憩入り」:状態の自動遷移
    「休憩」も一つの工数として計上するため、現在の作業を止めた直後に、自動で「休憩」という名前のタスクを開始
test.py
def start_break(self):
    # 1. 今の作業を終了打刻
    self.stop_current_task()
    
    # 2. 即座に『休憩』タスクとして新しい行を開始(終了時間は空のまま)
    self.current_task = "休憩"
    self.start_time = datetime.now()
    self.save_log(self.current_task, self.start_time, None) # 稼働中として記録
    
    # UI側の表示もオレンジ(休憩中)に切り替え
    self.status_label.configure(text="休憩中", text_color="orange")
  • 「業務終了」:記録の完全停止
    業務終了は「次」を開始しません。これにより、翌朝まで「休憩中」が延々と記録される状態を防止
test.py
def finish_work(self):
    if not self.current_task: return

    # 確認ダイアログで誤操作を防止
    if messagebox.askyesno("業務終了", "本日の全業務記録を完了しますか?"):
        # 最後の作業を閉じるだけで、新しいタスクは生成しない
        self.stop_current_task()
        
        # 全ての状態をクリア
        self.current_task = None
        self.start_time = None
        self.status_label.configure(text="業務終了", text_color="red")

Excelファースト

現場でよく使うのはExcelだと思われるため

  • UTF-8-SIG (BOM付き): すべてのCSV出力をBOM付きに固定。これにより、Excelでダブルクリックした瞬間に文字化けする現象を根絶
  • インポート/エクスポート: タスクリストをUI上でポチポチ編集させるのではなく、CSVで一括操作させることで、大量のタスク管理もExcelのオートフィルで対応可能に

  • UTF-8-SIG (BOM付き) による文字化け根絶
    Pythonのデフォルト(UTF-8)でCSVを吐くと、Excelで開いた瞬間に日本語が文字化けする。これを防ぐために、すべてのファイル入出力にutf-8-sigを指定し、ExcelにUTF-8であることを明示する
test.py
import csv

def save_csv_excel_friendly(file_path, data_rows):
    """
    Excelでダブルクリックした瞬間に正しく開けるCSVを生成する
    """
    with open(file_path, "w", encoding="utf-8-sig", newline="") as f:
        writer = csv.writer(f)
        # ヘッダーとデータを一気に書き込む
        writer.writerows(data_rows)
  • タスク管理の「Excelへの丸投げ」 (Import/Export)
    タスクリストの追加・削除画面をGUIの廃止
    代わりに、「テンプレートを出力 → Excelで一括編集 → 読み込み」というフローにすることで、100個のタスク追加もExcelのオートフィルで対応
test.py
# 管理者パネル:タスクリストのインポート処理(抜粋)
def import_tasks(self):
    path = filedialog.askopenfilename(filetypes=[("CSV", "*.csv")])
    if path:
        with open(path, "r", encoding="utf-8-sig") as f:
            reader = csv.reader(f)
            next(reader)  # 1行目のヘッダーをスキップ
            
            # Excelで編集された「タスク名」の列をリスト化
            new_tasks = [row[0] for row in reader if row]
            
        # UI上のテキストエリアに流し込み(人間が最後に確認して保存)
        self.task_text.delete("1.0", "end")
        self.task_text.insert("1.0", "\n".join(new_tasks))

低スペックPCでも動くように調整

  • リアルタイム監視のon/off機能:負荷が高い仕事中はオフにするなど、柔軟な対応をできるように
  • ms単位のポーリング制御: リアルタイム監視の間隔を管理者が調整。PCが重ければ間隔を広げ、余裕があれば1秒更新など調整可能

  • ms(ミリ秒)単位の可変ポーリング
    監視間隔をハードコーディングせず、config.jsonから動的に取得するように
    現場の負荷状況に合わせて、管理者それぞれが「1秒」から「10秒」まで、GUI上でいつでも調整可能
test.py
def refresh_monitor(self):
    # リアルタイム更新のチェックが入っている時だけ実行
    if self.auto_refresh.get():
        self.update_tree_view() # 全ログファイルの走査処理
        
    # 設定値(ms)を取得。入力エラーがあれば3000ms(3秒)で安全にフォールバック
    try:
        interval_ms = int(self.refresh_entry.get())
    except ValueError:
        interval_ms = 3000
    
    # 指定されたミリ秒後に自分自身を再実行
    self.root.after(interval_ms, self.refresh_monitor)
  • 差分なしの読み込み
    軽量化を意識
    データベースを使わず、かつネットワーク負荷を抑えるため、利用者側は「追記のみ」、管理者側は「最新の1行だけ見る」という役割分担を徹底
test.py
# 管理者側:全ファイルを走査するが、中身は「最後の一行」しか読み込まない
with open(log_file_path, "r", encoding="utf-8-sig") as f:
    lines = list(csv.reader(f))
    if len(lines) > 1:
        last_status = lines[-1]
        # 終了時間が空(未入力)= 現在稼働中と判定
        if not last_status[2]:
            self.display_active_user(pc_name, last_status)

PyInstaller オプションについて

  • --onefile
    .exe ファイルを一つ、共有フォルダに置くだけ
    「これをデスクトップにコピーして使ってください」という一言で導入が完結するように設計

  • --noconsole: コンソール画面を非表示に

  • --name "進捗入力くん"
    「ファイル名」もユーザーインターフェース
    デフォルト名だとuser_app.exeになるけど、これだと何のソフトか直感的にわからない

  • --clean
    PyInstallerはキャッシュが残ると、たまに謎のビルドエラーや実行時エラーを起こすことがあるため

pyinstaller --onefile --noconsole --name "進捗管理者パネル" admin_app.py

追記(2026/3/10)

リアルタイム進捗反映とログの整合性向上

  • ユーザー側:タスク開始時に仮登録
def switch_task(self):
    # (中略:既存タスクの停止処理など)

    # 1. 新しいタスクの情報をセット
    self.current_task = new_task
    self.start_time = datetime.now()

    # 2. 開始した瞬間にCSVに「終了空欄」で書き込む
    # これにより、管理者がファイルを読み込んだ時に「今のタスク」が見えるようになる
    self.save_log(self.current_task, self.start_time, None)
  • 管理者側:終了時間が空のデータを探す
# 管理者側の判定ロジック
with open(f, "r", encoding="utf-8-sig") as csv_f:
    r = list(csv.reader(csv_f))
    # 最終行の終了時間(インデックス2)が空かどうかをチェック
    if len(r) > 1 and not r[-1][2]:
        # 進行中としてTreeviewに表示
        start = datetime.strptime(r[-1][1], "%Y-%m-%d %H:%M:%S")
        elap = str(datetime.now() - start).split(".")[0]
        self.tree.insert("", "end", values=(name, r[-1][0], elap, adm))

※タスクは例

以前の仕組み(非リアルタイム)

  1. ユーザーが「英語」を開始 → (CSVには何も起きない)
  2. ユーザーが「数学」に切り替えた瞬間 → (ここで初めて英語のログがCSVに追記される)
  • 問題点: 切り替えるまでCSVが更新されないので、管理者は「今何をしているか」が全く分からない

今回の仕組み(リアルタイム)

  1. ユーザーが「英語」を開始 → 即座にCSVへ 英語, 開始時間, (空欄) と書き込む

  2. 管理者アプリがCSVを読み込む → 最終行が空欄なので「英語をやってるな」と判明

  • ユーザーが「数学」に切り替える → 前の行の (空欄) を 終了時間 で上書きし、新しく数学の行を作る

コード

こちらからどうぞ
https://github.com/kuranku817/personal-development/tree/main/StatusBoard

2026年3月10日更新

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?