feat: roster.json 단일 명단 파일 + 핫리로드
호스트에서 직접 편집 가능한 단일 JSON으로 명단 일원화.
앱이 매 요청마다 디스크에서 reload → 컨테이너 재시작 불필요.
변경:
- roster.json 새 형식: {"people": [{"name", "team", "dept", "senior", "notes"}, ...]}
- assign_teams.py: roster.json + legacy participants.json 둘 다 출력
- app.py: get_participants() / get_teams() 매 호출 reload
- PARTS = get_participants() / TEAMS = get_teams() 함수 안에서 호출
- 모듈 레벨 PARTICIPANTS/TEAMS 제거
- load_roster() roster.json 우선, 없으면 legacy fallback
- docker-compose: roster.json + participants.json 둘 다 mount
- Dockerfile: ROSTER env + roster.json COPY
사용자 워크플로:
- 사람 다른 팀 옮기기: roster.json에서 그 사람 'team' 값만 변경
- 자동 배정 재실행: python3 assign_teams.py
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,11 +12,12 @@ COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app.py assign_teams.py ./
|
||||
COPY participants.json ./
|
||||
COPY roster.json participants.json ./
|
||||
|
||||
EXPOSE 8501
|
||||
|
||||
ENV VOTE_DB=/data/votes.db \
|
||||
ROSTER=/app/roster.json \
|
||||
PARTICIPANTS=/app/participants.json
|
||||
|
||||
CMD ["streamlit", "run", "app.py", \
|
||||
|
||||
16
README.md
16
README.md
@@ -60,8 +60,20 @@ streamlit run app.py --server.address 0.0.0.0 --server.port 8501
|
||||
|
||||
## 데이터
|
||||
|
||||
- `participants.json` — 이름→팀 매핑 (`assign_teams.py` 산출물)
|
||||
- `votes.db` (sqlite) — 테이블: `votes(id, voter_name UNIQUE, voter_team, fun_team, polish_team, utility_team, created_at)`
|
||||
- `roster.json` — **단일 명단 파일** (`assign_teams.py` 산출물). 호스트에서 직접 편집 가능, 앱이 매 요청 reload.
|
||||
```json
|
||||
{
|
||||
"people": [
|
||||
{"name": "홍길동", "team": "팀1", "dept": "MLOps Data", "senior": true, "notes": ""},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
- 사람을 다른 팀으로 옮기기: `team` 값만 변경
|
||||
- 사람 추가/제거: 객체 추가/삭제
|
||||
- 변경 후 컨테이너 재시작 불필요 (핫리로드)
|
||||
- `participants.json` — legacy 형식 (이름→팀 string). roster.json 없으면 fallback.
|
||||
- `votes.db` (sqlite, WAL) — `votes`, `team_titles`, `tie_breaks`, `settings` 테이블
|
||||
|
||||
## 운영 팁
|
||||
|
||||
|
||||
62
app.py
62
app.py
@@ -13,23 +13,47 @@ import streamlit as st
|
||||
|
||||
DB_PATH = os.environ.get("VOTE_DB", "votes.db")
|
||||
ADMIN_TOKEN = os.environ.get("ADMIN_TOKEN", "change-me")
|
||||
PARTICIPANTS_PATH = os.environ.get(
|
||||
ROSTER_PATH = os.environ.get(
|
||||
"ROSTER", str(Path(__file__).parent / "roster.json")
|
||||
)
|
||||
LEGACY_PARTICIPANTS_PATH = os.environ.get(
|
||||
"PARTICIPANTS", str(Path(__file__).parent / "participants.json")
|
||||
)
|
||||
|
||||
|
||||
def load_participants():
|
||||
"""이름→팀 매핑. 파일 없으면 빈 dict."""
|
||||
p = Path(PARTICIPANTS_PATH)
|
||||
if not p.exists():
|
||||
return {}
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
def load_roster():
|
||||
"""
|
||||
roster.json 우선, 없으면 legacy participants.json fallback.
|
||||
매 호출마다 디스크에서 read → 호스트 편집 핫리로드.
|
||||
return: {"people": [...]} 형식 dict
|
||||
"""
|
||||
roster_p = Path(ROSTER_PATH)
|
||||
if roster_p.exists():
|
||||
return json.loads(roster_p.read_text(encoding="utf-8"))
|
||||
|
||||
legacy_p = Path(LEGACY_PARTICIPANTS_PATH)
|
||||
if legacy_p.exists():
|
||||
# legacy: {name: team_str}
|
||||
legacy = json.loads(legacy_p.read_text(encoding="utf-8"))
|
||||
return {
|
||||
"people": [
|
||||
{"name": n, "team": t, "dept": "", "senior": False, "notes": ""}
|
||||
for n, t in legacy.items()
|
||||
]
|
||||
}
|
||||
return {"people": []}
|
||||
|
||||
|
||||
PARTICIPANTS = load_participants()
|
||||
TEAMS = sorted(set(PARTICIPANTS.values())) if PARTICIPANTS else [
|
||||
f"팀{i}" for i in range(1, 8)
|
||||
]
|
||||
def get_participants():
|
||||
"""이름→팀 매핑. 매 호출 reload (호스트 편집 즉시 반영)."""
|
||||
roster = load_roster()
|
||||
return {p["name"]: p["team"] for p in roster.get("people", [])}
|
||||
|
||||
|
||||
def get_teams():
|
||||
"""팀명 정렬 list. 매 호출 reload."""
|
||||
parts = get_participants()
|
||||
return sorted(set(parts.values())) if parts else [f"팀{i}" for i in range(1, 8)]
|
||||
|
||||
CATEGORIES = [
|
||||
("fun_team", "🎉 재미상", "손선풍기 5개"),
|
||||
@@ -268,7 +292,9 @@ def render_voter():
|
||||
|
||||
st.caption("이름 선택 → 본인 팀 자동 매핑 → 본인 팀 제외 3분야 투표. 한 번만 제출 가능.")
|
||||
|
||||
if not PARTICIPANTS:
|
||||
PARTS = get_participants()
|
||||
TEAMS = get_teams()
|
||||
if not PARTS:
|
||||
st.error("참가자 명단이 없습니다. `assign_teams.py` 먼저 실행하세요.")
|
||||
return
|
||||
|
||||
@@ -284,7 +310,7 @@ def render_voter():
|
||||
|
||||
name = st.selectbox(
|
||||
"본인 이름",
|
||||
options=sorted(PARTICIPANTS.keys()),
|
||||
options=sorted(PARTS.keys()),
|
||||
index=None,
|
||||
placeholder="이름 선택",
|
||||
format_func=fmt_name,
|
||||
@@ -308,7 +334,7 @@ def render_voter():
|
||||
st.warning("사번 입력 후 진행하세요.")
|
||||
return
|
||||
|
||||
my_team = PARTICIPANTS[name]
|
||||
my_team = PARTS[name]
|
||||
titles = get_titles()
|
||||
st.info(f"본인 팀: **{fmt_team(my_team, titles)}**")
|
||||
candidates = [t for t in TEAMS if t != my_team]
|
||||
@@ -382,10 +408,12 @@ def render_admin():
|
||||
|
||||
st.divider()
|
||||
|
||||
PARTS = get_participants()
|
||||
TEAMS = get_teams()
|
||||
conn = get_conn()
|
||||
total = conn.execute("SELECT COUNT(*) FROM votes").fetchone()[0]
|
||||
|
||||
expected = len(PARTICIPANTS) if PARTICIPANTS else None
|
||||
expected = len(PARTS) if PARTS else None
|
||||
col_a, col_b, col_c = st.columns(3)
|
||||
col_a.metric("투표 참여자", f"{total}명")
|
||||
col_b.metric("팀 수", f"{len(TEAMS)}팀")
|
||||
@@ -395,10 +423,10 @@ def render_admin():
|
||||
|
||||
if expected and total < expected:
|
||||
voted = {row[0] for row in conn.execute("SELECT voter_name FROM votes").fetchall()}
|
||||
not_voted = [n for n in PARTICIPANTS.keys() if n not in voted]
|
||||
not_voted = [n for n in PARTS.keys() if n not in voted]
|
||||
with st.expander(f"⏳ 미투표자 ({len(not_voted)}명)"):
|
||||
for n in sorted(not_voted):
|
||||
st.write(f"- {n} ({PARTICIPANTS[n]})")
|
||||
st.write(f"- {n} ({PARTS[n]})")
|
||||
|
||||
st.divider()
|
||||
st.subheader("📝 팀별 결과물 제목 입력")
|
||||
|
||||
@@ -240,12 +240,40 @@ def main():
|
||||
mapping[name] = team_name
|
||||
print()
|
||||
|
||||
# roster.json (단일 파일, 호스트에서 직접 편집 가능)
|
||||
people_records = []
|
||||
for i, team in enumerate(teams, 1):
|
||||
team_name = f"팀{i}"
|
||||
for name, dept in team:
|
||||
note_parts = []
|
||||
if name == "한지승":
|
||||
note_parts.append("지각 가능")
|
||||
if name == "김재현":
|
||||
note_parts.append("최주니어")
|
||||
people_records.append(
|
||||
{
|
||||
"name": name,
|
||||
"team": team_name,
|
||||
"dept": dept,
|
||||
"senior": name in SENIORS,
|
||||
"notes": ", ".join(note_parts),
|
||||
}
|
||||
)
|
||||
|
||||
roster_path = Path(__file__).parent / "roster.json"
|
||||
roster_path.write_text(
|
||||
json.dumps({"people": people_records}, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"저장: {roster_path}")
|
||||
|
||||
# 구 형식 호환 (legacy participants.json 도 함께 저장 — 마이그레이션 기간)
|
||||
out = Path(__file__).parent / "participants.json"
|
||||
out.write_text(
|
||||
json.dumps(mapping, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"저장: {out}")
|
||||
print(f"저장: {out} (legacy)")
|
||||
|
||||
# teams.md 박제 (진행자 인쇄/공유용)
|
||||
md_lines = ["# 해커톤 팀 배정 (확정)\n"]
|
||||
|
||||
@@ -8,11 +8,14 @@ services:
|
||||
environment:
|
||||
ADMIN_TOKEN: ${ADMIN_TOKEN:-change-me}
|
||||
VOTE_DB: /data/votes.db
|
||||
ROSTER: /app/roster.json
|
||||
PARTICIPANTS: /app/participants.json
|
||||
volumes:
|
||||
# 호스트의 participants.json 변경 즉시 반영 (재배정 시)
|
||||
# roster.json 호스트 편집 즉시 반영 (앱이 매 요청 reload)
|
||||
- ./roster.json:/app/roster.json:ro
|
||||
# legacy participants.json fallback
|
||||
- ./participants.json:/app/participants.json:ro
|
||||
# 투표 DB는 호스트에 영속 (컨테이너 재시작해도 유지)
|
||||
# 투표 DB 영속
|
||||
- vote-data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
242
roster.json
Normal file
242
roster.json
Normal file
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"people": [
|
||||
{
|
||||
"name": "김호승",
|
||||
"team": "팀1",
|
||||
"dept": "AI Efficiency Tech",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "유준희",
|
||||
"team": "팀1",
|
||||
"dept": "AI Efficiency Tech",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "이준석",
|
||||
"team": "팀1",
|
||||
"dept": "MLOps HPC",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "장다현",
|
||||
"team": "팀1",
|
||||
"dept": "MLOps System",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "강승형",
|
||||
"team": "팀1",
|
||||
"dept": "MLOps Data",
|
||||
"senior": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "서한배",
|
||||
"team": "팀2",
|
||||
"dept": "AI Efficiency Tech",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "김민섭",
|
||||
"team": "팀2",
|
||||
"dept": "AI Efficiency Tech",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "유용혁",
|
||||
"team": "팀2",
|
||||
"dept": "MLOps HPC",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "박영훈",
|
||||
"team": "팀2",
|
||||
"dept": "MLOps System",
|
||||
"senior": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "박재호",
|
||||
"team": "팀2",
|
||||
"dept": "MLOps Data",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "이성재",
|
||||
"team": "팀3",
|
||||
"dept": "AI Efficiency Tech",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "이재광",
|
||||
"team": "팀3",
|
||||
"dept": "AI Efficiency Tech",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "김영관",
|
||||
"team": "팀3",
|
||||
"dept": "MLOps HPC",
|
||||
"senior": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "정채윤",
|
||||
"team": "팀3",
|
||||
"dept": "MLOps System",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "변수민",
|
||||
"team": "팀3",
|
||||
"dept": "MLOps Data",
|
||||
"senior": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "심성환",
|
||||
"team": "팀4",
|
||||
"dept": "AI Efficiency Tech",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "유지원",
|
||||
"team": "팀4",
|
||||
"dept": "AI Efficiency Tech",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "오근현",
|
||||
"team": "팀4",
|
||||
"dept": "MLOps HPC",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "장혁진",
|
||||
"team": "팀4",
|
||||
"dept": "MLOps System",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "손현준",
|
||||
"team": "팀4",
|
||||
"dept": "MLOps Data",
|
||||
"senior": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "정현준",
|
||||
"team": "팀5",
|
||||
"dept": "AI Efficiency Tech",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "조민정",
|
||||
"team": "팀5",
|
||||
"dept": "AI Efficiency Tech",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "김재현",
|
||||
"team": "팀5",
|
||||
"dept": "MLOps HPC",
|
||||
"senior": false,
|
||||
"notes": "최주니어"
|
||||
},
|
||||
{
|
||||
"name": "김병훈",
|
||||
"team": "팀5",
|
||||
"dept": "MLOps System",
|
||||
"senior": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "한지승",
|
||||
"team": "팀5",
|
||||
"dept": "MLOps Platform",
|
||||
"senior": true,
|
||||
"notes": "지각 가능"
|
||||
},
|
||||
{
|
||||
"name": "이정태",
|
||||
"team": "팀6",
|
||||
"dept": "AI Efficiency Tech",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "최호진",
|
||||
"team": "팀6",
|
||||
"dept": "MLOps HPC",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "김정명",
|
||||
"team": "팀6",
|
||||
"dept": "MLOps HPC",
|
||||
"senior": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "길주현",
|
||||
"team": "팀6",
|
||||
"dept": "MLOps System",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "서희",
|
||||
"team": "팀6",
|
||||
"dept": "MLOps System",
|
||||
"senior": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "이준형",
|
||||
"team": "팀7",
|
||||
"dept": "AI Efficiency Tech",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "전효준",
|
||||
"team": "팀7",
|
||||
"dept": "MLOps HPC",
|
||||
"senior": true,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "이지환",
|
||||
"team": "팀7",
|
||||
"dept": "MLOps System",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "김동국",
|
||||
"team": "팀7",
|
||||
"dept": "MLOps Data",
|
||||
"senior": false,
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user