""" 참가자 34명 → 7팀 배정 (5명 6팀 + 4명 1팀). - 부서별 균등 분배 (max - min ≤ 1) - 시니어 균등 분배 (max - min ≤ 1, 모든 팀 ≥1) - 김태현은 진행요원 (참가 X) - AI Efficiency Tech는 시니어/주니어 정보 없음 → 균등 분배 대상 외 결과: participants.json (이름→팀 매핑) + 콘솔 출력. """ import json import random from collections import Counter, defaultdict from pathlib import Path DEFAULT_TOPICS_SEED = [ { "id": "T1", "title": "내 인생 시간 도둑 처단기", "tagline": "매일 짜증나는 반복 작업 하나를 2시간 안에 박살내기.", "tone": "실용 + 살짝 치트키", "items": [ "Slack 멘션 자동 분류/요약기 — '진짜 날 부른 거' vs 'FYI' 분리", "Jira 티켓 1줄 자동 요약 + 다음 액션 제안기", "회의 캘린더 → 하루 시작 브리핑 (\"오늘 3개 있고 2개는 안 가도 됨\")", "PR 리뷰 우선순위 큐 (크기/긴급도/차단여부 기반)", "반복 쿼리/kubectl 명령 매크로 CLI — 자주 치는 10개를 1글자로", "온콜 노이즈 필터 — 진짜 볼 알람 vs 무시해도 되는 알람", "\"이번 주 내 활동 자동 요약\" — PR/티켓/리뷰 통합 리포트", "Grafana 자주 보는 패널 즐겨찾기 통합 뷰", "Slack 스레드 장문 요약기 — 놓친 채널 따라잡기용", "\"이 회의 들어가야 함?\" 분류기 — 캘린더 제목·참석자 기반 추천", ], }, { "id": "T2", "title": "벼르던 사이드 프로젝트", "tagline": "평소 \"저거 하나 만들고 싶은데\" 하던 개인 토이를 2시간 안에 작은 완성품으로.", "tone": "몰입 + 작은 완결성", "items": [ "내 PR/커밋 패턴 분석 개인 대시보드 (시간대/요일/사이즈 분포)", "팀 Wiki/Notion을 터미널에서 fzf 스타일로 검색하는 CLI", "사내 모델/데이터셋 메타데이터 검색 프로토타입", "git 히스토리 인터랙티브 시각화 뷰어", "\"오늘 내가 한 일\" 자동 일기 생성기 (커밋/PR/티켓 통합)", "PR 코멘트 감정/톤 분석으로 팀 리뷰 문화 리포트", "로컬 Kubernetes 리소스 관계 그래프 실시간 시각화", "사내 논문/테크 문서 RAG 검색 도구", "터미널에서 차트 포함된 마크다운 뷰어", "북마크/링크 자동 분류·태깅 개인 도구", ], }, { "id": "T3", "title": "오버엔지니어링 선수권", "tagline": "평소 안 써본 무거운 기술 패턴을 일부러 작은 문제에 적용해 배우기.", "tone": "학습형 오버엔지니어링", "items": [ "Todo 앱에 이벤트 소싱 + CQRS 제대로 적용", "간단한 계산기 서비스에 OpenTelemetry 풀 트레이싱 구축", "문서 검색 기능에 벡터 DB + 하이브리드 검색 (BM25 + 임베딩)", "파일 업로드에 S3 presigned URL + 체크섬 검증 + 재시도 로직 정식 설계", "팀 투표 기능을 Raft 합의 알고리즘으로 구현", "회의실 예약을 Kafka 이벤트 스트리밍 기반으로", "로컬 개발 환경을 완전한 K8s 매니페스트 (Deployment/Service/Ingress/HPA)로 재현", "LLM + RAG 기반 PR 자동 리뷰 봇 아키텍처 설계·구현", "멀티 에이전트 협업(프롬프트 2~3단계)으로 간단한 의사결정 시스템", "사이드카 패턴으로 로깅/메트릭/인증 분리 데모", ], }, { "id": "T4", "title": "팀에게 주는 작은 선물", "tagline": "동료를 돕는 도구/봇/사이트. 특정인을 놀리는 게 아니라 팀 전체를 위한 것.", "tone": "실질적 도움 + 가벼운 온기", "items": [ "배포 상태 집계·알림 봇 (성공/실패/롤백 요약)", "신입 한 주 서바이벌 가이드 자동 생성기 (온보딩 링크/문서 수집)", "팀 내부 용어/약어 사전 봇 — 신입/리서처 친화", "아침 브리핑 봇 — 오늘 회의/배포/만료 알람 한방", "점심 투표 1분 컷 봇 — 선택지 자동 생성 후 이모지 투표", "팀 반복 질문 FAQ 봇 — 같은 질문 반복되는 채널용", "온콜 교대 시 인수인계 자동 요약 생성기", "회의실 스마트 추천 — 인원/시간대/위치 기반", "사내 서비스 변경사항 요약 구독 봇", "\"이번 주 팀 지표 한 장\" 리포트 — 머지 PR, 해결 티켓, 배포 수", ], }, ] def ensure_topics_seeded(data): """topics 비어있으면 default 시드. 기존 있으면 보존.""" cats = data.get("topics", {}).get("categories", []) if not cats: data.setdefault("topics", {}) data["topics"]["categories"] = DEFAULT_TOPICS_SEED # 시니어 명단 (Platform/Data/HPC/System만 알고 있음) SENIORS = { "한지승", "손현준", "강승형", "변수민", # Platform/Data "김정명", "김영관", "전효준", # HPC "박영훈", "서희", "김병훈", # System } PEOPLE = [ # (name, dept) ("한지승", "MLOps Platform"), ("변수민", "MLOps Data"), ("박재호", "MLOps Data"), ("강승형", "MLOps Data"), ("손현준", "MLOps Data"), ("김동국", "MLOps Data"), # 김태현 진행요원 (제외) ("김재현", "MLOps HPC"), ("이준석", "MLOps HPC"), ("오근현", "MLOps HPC"), ("김정명", "MLOps HPC"), ("김영관", "MLOps HPC"), ("유용혁", "MLOps HPC"), ("최호진", "MLOps HPC"), ("전효준", "MLOps HPC"), ("김병훈", "MLOps System"), ("이지환", "MLOps System"), ("서희", "MLOps System"), ("정채윤", "MLOps System"), ("장혁진", "MLOps System"), ("장다현", "MLOps System"), ("박영훈", "MLOps System"), ("길주현", "MLOps System"), ("조민정", "AI Efficiency Tech"), ("김민섭", "AI Efficiency Tech"), ("김호승", "AI Efficiency Tech"), ("서한배", "AI Efficiency Tech"), ("심성환", "AI Efficiency Tech"), ("유준희", "AI Efficiency Tech"), ("이성재", "AI Efficiency Tech"), ("이재광", "AI Efficiency Tech"), ("이정태", "AI Efficiency Tech"), ("이준형", "AI Efficiency Tech"), ("정현준", "AI Efficiency Tech"), ("유지원", "AI Efficiency Tech"), ] NUM_TEAMS = 7 # 34명 / 7팀 = 5명 6팀 + 4명 1팀 TEAM_SIZES = [5] * 6 + [4] SEED = 20260428 # 행사일 시드 (재현 가능) # 자동 배정 후 수동 swap (이름 쌍) MANUAL_SWAPS = [ ("김영관", "이준석"), ] def assign_one(seed): """ 가변 팀 사이즈 균등 분배. 각 사람을 한 명씩 할당: - 우선순위 1: 남은 슬롯 많은 팀 - 우선순위 2: 이 부서 인원 적게 받은 팀 - tie: random 효과: - 팀 사이즈 정확히 TEAM_SIZES (가변) - 부서별 max - min ≤ 1 (자연스럽게) - 시니어 균등은 시드 재시도로 검증 """ rng = random.Random(seed) by_dept = defaultdict(list) for name, dept in PEOPLE: by_dept[dept].append(name) for d in by_dept: rng.shuffle(by_dept[d]) teams = [[] for _ in range(NUM_TEAMS)] remaining = TEAM_SIZES[:] depts_sorted = sorted(by_dept.keys(), key=lambda d: -len(by_dept[d])) for dept in depts_sorted: for person in by_dept[dept]: dept_counts = [Counter(d for _, d in t).get(dept, 0) for t in teams] priority = sorted( range(NUM_TEAMS), key=lambda i: ( -remaining[i], dept_counts[i], rng.random(), ), ) for ti in priority: if remaining[ti] > 0: teams[ti].append((person, dept)) remaining[ti] -= 1 break return teams def validate(teams): """모든 제약 검증.""" sizes = [len(t) for t in teams] if sorted(sizes, reverse=True) != sorted(TEAM_SIZES, reverse=True): return False, "팀 사이즈 불일치" # 부서별 max - min ≤ 1 all_depts = {d for _, d in PEOPLE} for dept in all_depts: per_team = [Counter(d for _, d in t).get(dept, 0) for t in teams] if max(per_team) - min(per_team) > 1: return False, f"부서 {dept} 분포 불균등: {per_team}" # EffTech 모든 팀 ≥1 for ti, team in enumerate(teams): eff = sum(1 for _, d in team if d == "AI Efficiency Tech") if eff < 1: return False, f"팀{ti+1} EffTech 0명" # 시니어 검증 senior_per_team = [ sum(1 for n, _ in t if n in SENIORS) for t in teams ] if min(senior_per_team) < 1: return False, f"시니어 0명 팀 존재: {senior_per_team}" if max(senior_per_team) - min(senior_per_team) > 1: return False, f"시니어 분포 불균등: {senior_per_team}" # 한지승(지각 가능) 특수 제약 # 1. 4명 팀에 배치 금지 (지각 시 3명 → 너무 적음) # 2. 한지승 팀에 다른 시니어 ≥1명 (지각 시 시니어 0 방지) for team in teams: names = [n for n, _ in team] if "한지승" in names: if len(team) < 5: return False, "한지승 4명팀 배치 (지각 시 3명)" other_seniors = sum(1 for n in names if n in SENIORS and n != "한지승") if other_seniors < 1: return False, "한지승 팀에 다른 시니어 0명 (지각 시 시니어 0)" return True, "OK" def apply_swaps(teams, swaps): """이름 쌍을 받아 두 사람의 팀 위치를 교환.""" name_to_loc = {} for ti, team in enumerate(teams): for pi, (name, _) in enumerate(team): name_to_loc[name] = (ti, pi) for a, b in swaps: if a not in name_to_loc or b not in name_to_loc: raise ValueError(f"swap 대상 못 찾음: {a}, {b}") ti_a, pi_a = name_to_loc[a] ti_b, pi_b = name_to_loc[b] if ti_a == ti_b: continue # 같은 팀이면 의미 없음 teams[ti_a][pi_a], teams[ti_b][pi_b] = ( teams[ti_b][pi_b], teams[ti_a][pi_a], ) # 위치 갱신 name_to_loc[a] = (ti_b, pi_b) name_to_loc[b] = (ti_a, pi_a) return teams def assign(base_seed): """시드 재시도로 모든 제약 만족하는 배정 찾기. 이후 수동 swap 적용.""" for offset in range(10000): seed = base_seed + offset teams = assign_one(seed) ok, msg = validate(teams) if ok: teams = apply_swaps(teams, MANUAL_SWAPS) return teams, seed, offset + 1 raise RuntimeError("10000회 시도에도 모든 제약 만족 실패") def main(): assert len(PEOPLE) == sum(TEAM_SIZES), ( f"인원수 불일치: {len(PEOPLE)}명, 기대 {sum(TEAM_SIZES)}명" ) teams, seed_used, attempts = assign(SEED) print(f"# 시드 {seed_used} (시도 {attempts}회), 팀 사이즈 {TEAM_SIZES}\n") # 부서별 분포 검증 dept_dist = defaultdict(list) for team in teams: team_counts = Counter(d for _, d in team) for dept in {d for _, d in PEOPLE}: dept_dist[dept].append(team_counts.get(dept, 0)) print("# 부서별 팀 분배 (max - min ≤ 1 = 균등)") for dept, dist in sorted(dept_dist.items(), key=lambda x: -sum(x[1])): print(f" {dept}: {dist} (총 {sum(dist)}명)") senior_dist = [sum(1 for n, _ in t if n in SENIORS) for t in teams] print(f"\n# 시니어 분배: {senior_dist} (총 {sum(senior_dist)}명, 모든 팀 ≥1)") print() mapping = {} for i, team in enumerate(teams, 1): team_name = f"팀{i}" dept_counts = Counter(d for _, d in team) dept_summary = ", ".join( f"{d.replace('MLOps ', '').replace('AI Efficiency Tech', 'AI Eff')} {c}" for d, c in dept_counts.items() ) n_senior = sum(1 for n, _ in team if n in SENIORS) size = len(team) late_note = " ⏰한지승 지각시 -1" if any(n == "한지승" for n, _ in team) else "" print(f"## {team_name} ({size}명, 시니어 {n_senior}, {dept_summary}){late_note}") for name, dept in team: tag = " ⭐시니어" if name in SENIORS else "" if name == "김재현": tag += " 🌱최주니어" if name == "한지승": tag += " ⏰지각가능" print(f" - {name} ({dept}){tag}") mapping[name] = team_name print() # 단일 hackathon.json (모든 state 포함). 기존 파일 있으면 votes/titles 등 보존. 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), } ) data_path = Path(__file__).parent / "hackathon.json" if data_path.exists(): data = json.loads(data_path.read_text(encoding="utf-8")) else: data = { "settings": {"voting_open": True, "current_stage": "intro"}, "titles": {}, "tie_breaks": {}, "votes": [], } data["people"] = people_records ensure_topics_seeded(data) # 신규 # 누락 키 보강 for k, v in [ ("settings", {"voting_open": True, "current_stage": "intro"}), ("titles", {}), ("tie_breaks", {}), ("votes", []), ]: data.setdefault(k, v) # 기존 settings에 누락된 nested 키 보강 data["settings"].setdefault("voting_open", True) data["settings"].setdefault("current_stage", "intro") data_path.write_text( json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8", ) print(f"저장: {data_path}") # teams.md 박제 (진행자 인쇄/공유용) md_lines = ["# 해커톤 팀 배정 (확정)\n"] md_lines.append(f"- 시드: `{seed_used}`\n") md_lines.append(f"- 총 {len(PEOPLE)}명, {NUM_TEAMS}팀, 사이즈 {TEAM_SIZES}\n") md_lines.append("- 김태현: 진행요원 (참여 X)\n") md_lines.append("- ⭐ 시니어, 🌱 최주니어, ⏰ 지각 가능\n\n") md_lines.append("| 팀 | 인원 | 시니어 | 멤버 |\n|---|---|---|---|\n") for i, team in enumerate(teams, 1): names_fmt = [] for name, _ in team: t = name if name in SENIORS: t += "⭐" if name == "김재현": t += "🌱" if name == "한지승": t += "⏰" names_fmt.append(t) n_senior = sum(1 for n, _ in team if n in SENIORS) md_lines.append( f"| 팀{i} | {len(team)} | {n_senior} | {', '.join(names_fmt)} |\n" ) md_out = Path(__file__).parent / "teams.md" md_out.write_text("".join(md_lines), encoding="utf-8") print(f"저장: {md_out}") if __name__ == "__main__": main()