Imported from Busy-Man/busy-man (
AGENTS.md). Install upstream withnpx skills add Busy-Man/busy-man. Copyright stays with the author.
AGENTS.md
이 저장소에서 작업하는 사람과 에이전트가 공통으로 따르는 규약. 이 파일이 정본이다.
CLAUDE.md는 이 파일을 가리키는 포인터일 뿐이므로, 규약을 고칠 때는 여기를 고친다.
프로젝트
NHN NAN 2026 (Game × AI) 사전 과제 출품작. 2인 팀, 제출 마감 2026-08-10. 출근길을 달리면서 고객사 문자에 답하는 브라우저 게임. 전방과 폰을 번갈아 보게 만드는 것이 코어.
- 대회 요건·제출물 5종·제한사항 →
docs/대회_정보.md(사실관계 충돌 시 이 문서가 우선) - 구현 담당 →
docs/역할 분담.txt(맵·주행 / 대화·질문 이벤트) - 일정과 대외 산출물 담당 →
docs/team-plan.html - 게임 기획 →
docs/기획_1차_보완.md(8/3의docs/기획 초안.md를 개정한 것) - 폰·질문 UI →
docs/ui-spec.md - 8/6 밸런싱 할 일 →
docs/balance-todo.md - 렌더러 결정 근거 →
docs/renderer-comparison.html
1. 파일 소유권 — 가장 중요한 규칙
둘 다 AI로 코드를 뽑는다. 같은 파일을 둘이 AI에게 던지면 서로의 코드를 덮어쓴다. 그래서 분담 기준은 난이도가 아니라 파일이다. 경계선은 "화면 위 / 폰 안".
| 파일 | 소유 | 범위 |
|---|---|---|
src/world.js |
A | 맵 렌더, 자동 전진, 좌우 이동, 좌/우회전, 갈림길, 보행자 충돌 감속, 게이지 가속 |
src/main.js |
A | 게임 루프, 입력 라우팅, 제한시간, HUD, 결과 화면 |
index.html |
A | 진입점 (main.js를 부르므로 여기 붙는다) |
src/phone.js |
B | 폰 UI, 대화 스트림 |
src/quiz.js |
B | 질문 모달, 질문 도착 주기, 보기 3개, 정오답, 패널티 |
content/*.json |
B | 대화 로그, 질문, 정답, 함정 단서 |
src/state.js |
공동 | 접점 4개. 아래 참조 |
vendor/ |
공동 | 동봉한 외부 라이브러리(three.js). 수정하지 않는다 |
docs/, assets/ |
공동 | |
.agents/ |
공동 | 훅·스킬 정본. .claude/·.codex/는 여기를 가리킨다 (§4) |
코드 밖의 일은 docs/team-plan.html 그대로다 — B가 배포·최종 제출,
A가 영상 촬영·문서 취합을 맡는다.
구현 담당의 근거는 docs/역할 분담.txt(A=맵·주행, B=대화·질문 이벤트)이고,
코드 소유권 정본은 위 표다. docs/team-plan.html은 2026-08-03에 이 기준으로 정정됐다.
남의 파일을 고치지 않는다. 고쳐야 할 이유가 생기면 고치지 말고 상대에게 말한다.
2. 접점은 네 개뿐
A와 B가 주고받는 것은 src/state.js의 이것뿐이다.
문서로 합의하면 지켜지지 않는다. 파일이 있으면 둘 다 그걸 import 한다 (docs/team-plan.html §03).
state.speedMul B가 오답 감속으로 낮춤 → A가 주행 속도에 곱함
state.gauge B가 정답 +, 오답 랜덤 시 − → A가 W로 소비
state.quizOpen B가 모달 뜨면 true → A는 이동·입력 정지
onGate(cb) B가 등록 → A가 갈림길에서 fireGate 로 발화
2026-08-04 정정. 위 네 줄의 A/B가 §1의 소유권 표와 반대로 적혀 있었다.
docs/team-plan.html의 A/B 역할을 8/3에 맞바꿀 때 §03의 접점 블록만 빠졌고, 이 표가 그것을 그대로 복사한 것이다. 라벨만 뒤집혀 있었으므로 방향의 의미는 바뀌지 않는다.
이름만 맞추면 8/5에 같은 논쟁을 다시 한다. 의미도 같이 못 박는다.
speedMul은 퀴즈 패널티 전용 채널이다. 보행자 충돌 감속을 여기 쓰지 않는다 — 두 주체가 각자 복귀 타이머를 걸면 마지막 기록자가 이겨 서로를 지운다. 충돌 감속은world.js안에 두고SPEED * state.speedMul * collisionMul로 곱한다gauge는 정답에 +, 오답·타임아웃에 −. 8/5 오전까지는 정답 전용이었으나, 같은 날 패널티 랜덤 부여를 되살리면서 감소 방향이 열렸다 → §3. 기록자는 여전히 B 하나다 — A는 W 소비 외에 여기에 쓰지 않는다. 둘이 쓰기 시작하면speedMul에서 막아 둔 것과 같은 사고가 난다- 게이지가 0이면 감소가 아무 일도 하지 않는다. 이건 알고 받아들인 구멍이다.
docs/game_balance_review.html§02가 *"못하는 플레이어일수록 오답이 저렴해지고, 잘하는 플레이어일수록 오답 1회가 비싸다"*고 지적한 그 자리다. 그럼에도 살린 것은 수치를 경험으로 보기로 했기 때문이고, 0일 때 감속으로 대체할지는 아직 정해지지 않았다 →docs/balance-todo.md onGate는 A가 발화하고 B가 듣는다. 등록은onGate(cb), 발화는fireGate(gateIndex)이고 둘은 한 접점의 두 짝이다 — 다섯 번째가 아니다. 표가 소유자를 안 적어 두면 양쪽이 서로를 기다리는 코드를 쓰고, 그때는 예외도 안 난다- ⚠️
onGate는 갈림길 신호이지 질문 신호가 아니다. 지금은 듣는 쪽이 없다. 질문은quiz.js가 자기 주기(6~8초)로 띄운다 —docs/기획_1차_보완.md§질문 이벤트다. 8/4 구현이 질문을 여기 물려 두었는데, 그러면 질문 수가 맵 구조에 묶여 주기가 성립하지 않는다. 남은 소비처는 길 안내뿐이고 그건 §3의 열림 항목이라 아직 아무도 등록하지 않는다. A는fireGate를 그대로 부르면 된다 — 듣는 쪽이 없어도 예외는 나지 않는다
접점을 다섯 번째로 늘리지 않는다. 늘려야 한다고 판단되면 코드를 쓰기 전에 상대와 합의한다.
state.js를 고치면 반드시 상대에게 알린다.
모듈이 밖으로 내주는 함수(phone.setVisible 같은 것)는 접점이 아니다. 공유하는 값이 아니라
한쪽이 다른 쪽을 부르는 단방향 호출이다. 다만 8/5에 알려야 할 목록에는 올라간다 —
안 알리면 상대가 안 부르고, 그때는 조용히 아무 일도 일어나지 않는다.
3. 확정된 것 / 아직 열려 있는 것
등급이 세 개다. 이전 판은 "확정 / 열림" 둘뿐이었는데, 그러면 *"수치는 구현 후 경험으로 조절"*이라고 스스로 적어 둔 항목까지 되돌릴 수 없는 것과 같은 칸에 앉는다. 8/6에 밸런싱을 하려면 어느 것을 만져도 되는지가 등급으로 보여야 한다.
| 등급 | 뜻 |
|---|---|
| 확정 | 되돌리지 않는다. 되돌리려면 상대와 합의부터 한다 |
| 잠정 | 방향은 정해졌고 코드를 써도 된다. 다만 수치와 연출은 8/6에 바뀔 수 있다 |
| 열림 | 합의 전에 코드를 쓰지 않는다 |
확정 — 되돌리지 않는다
- 번들러 없음. HTML + ES 모듈. Vite 등을 끼우지 않는다
- 렌더러는 three.js. 저장소에 동봉(
vendor/)하고 상대경로로 import한다. CDN을 쓰지 않는다 — 심사 당일 외부 서버가 죽거나 막히면 게임이 백지가 된다 - 질문은 모달 방식. 뜨는 동안 이동 정지 + 시간 정지 + 다른 UI 상호작용 불가. 카운트다운은 10초 (8/3에는 6초였다)
- 오답과 타임아웃의 패널티는 같다. 둘 다 같은 2종에서 뽑는다 (아래 「잠정」 참조)
- 가속은 게이지(0~100%, 정답 +25%·오답/타임아웃 랜덤 감속 시 −15%). 정답 시 충전,
W로 소비 (8/7 전엔 Shift였다 — B와 합의 후 정정,
docs/기획_1차_보완.md§조작 참고). 가속 중에는 무적 — 사람만 밀치고 지나간다. 속도는 6 → 12 - 역할은 직장인 1종
잠정 — 코드는 써도 되고, 8/6에 바뀔 수 있다
- 스테이지 1분 30초 ~ 2분 30초 (8/3에는 90초 고정이었다)
- 좌/우회전이 되살아났다. 방향키로 회전하고 카메라는 temple run 방식.
기획 원문이
(눈으로 확인 필요)를 달아 두었으므로 연출은 확정이 아니다 - 맵은 사전 생성 7종을 매판 랜덤으로 뽑는다. 원문 표기가
(변동 가능)이다 - 오답·타임아웃 패널티는 감속 / 게이지 감소 2종 중 랜덤이다. 8/5에 되살렸다 —
기획 원문(
docs/기획_1차_보완.md)의 *"2 中 랜덤"*이 그대로 산다. 비율도 감소량도 정해진 적이 없다. 8/6에 경험으로 본다 →docs/balance-todo.md - 랜덤 부여 연출(룰렛·카드 뽑기)은 아직 보류다. 판정은 살아났지만 연출 에셋이 0인 사정은 그대로다. 지금은 어느 쪽이 걸렸는지 모달에 결과만 적는다
- 퀴즈 오답 감속과 보행자 충돌 감속의 정도가 지금은 같다. 갈라야 하는지는 경험으로 본다
- 스크랩 없이 간다. 폐기 확정은 아니다 — 시간이 남으면 되살릴 수 있다
- 질문 폰 UI는
docs/ui-spec.md— 구성과 입력 방식이 거기 있다
열림 — 합의 전에 코드를 쓰지 않는다
content/day1.json스키마 — B가 AI로 뽑는 JSON과phone.js·quiz.js가 기대하는 모양이 어긋나면 8/5 병합일에 드러난다. 그날은 고칠 시간이 없다- 질문 개수가 정해진 적이 없다. 있는 것은 주기 6
8초와 스테이지 90150초뿐이고, 그대로 두면 문항이 20개 안팎이 된다. 개수를 먼저 정할지 주기를 정할지 고른다 - 길 안내(네비)를 누가 갖는가 — 경로가 랜덤이 아니게 되면서 §1의 파일 경계에 걸쳤다.
안내 문구는
content/*.json(B)인데 갈림길 판정은world.js(A)다
밸런스 검토 판정 — docs/game_balance_review.html (8/1)
그 문서는 8/1 시점 기록이고 정본이 아니다. 계산은 참고하되 결론을 그대로 따르지 않는다. 고치지도 않는다 — HTML이라 diff가 읽히지 않는다. 권고 7건이 어떻게 됐는지만 여기 남긴다.
| 검토 § | 권고 | 판정 |
|---|---|---|
| §02 | 오답 시 게이지 감소는 상벌이 실력과 역상관이다 | 알고 받아들였다 — 8/5에 되살렸다. 게이지 0일 때 처리는 8/6 |
| §03 | 스크랩 슬롯 5→3, 오답 시 랜덤 삭제 제거 | 스크랩을 이번 스코프에서 뺐다 (보류) |
| §04① | 랜덤 패널티는 표본이 부족하다 | 따르지 않는다 — 8/5에 랜덤을 되살렸다. 표본이 실제로 부족한지는 8/6에 본다 |
| §04② | 게이지 상한에서 정답 가치가 0이 된다 | 안 고친다 — 게이지 관리는 유저 몫으로 둔다 |
| §04③ | 부스트 중 사람을 밀치게 하라 | 채택 — 가속 = 무적 + 속도 |
| §04④ | 생명을 없애고 시간 단일 축으로 | 채택 — 생명 개념이 없다 |
| §05 | 무응답 벌은 오답 벌보다 작게 | 따르지 않는다 — 타임아웃과 오답을 같게 뒀다. 찍기 쪽으로 기우는지는 8/6에 본다 |
2026-08-05 재조정.
docs/기획_1차_보완.md를 새 기준으로 삼아 위 목록을 갱신했다. 8/3 확정에서 되살아난 것은 좌/우회전과 맵이고, 새로 정해진 것은 카운트다운 10초와 가속 무적이다. 접점은 여전히 네 개다 — 실시간 방식도 검토했으나 모달을 유지했기 때문이다.다만 같은 날 늦게 패널티 랜덤 부여를 되살리면서
gauge의 의미가 바뀌었다. 개수가 는 것이 아니라 채널 하나에 감소 방향이 열린 것이고,src/state.js의 주석도 함께 고쳤다 — 규약과 코드가 두 벌로 어긋나면 8/5 병합 때 상대가 읽는 것은 코드 쪽이다.
4. AI 활용 기록 — 빠뜨리면 제출물이 빈다
제출물 4번(AI 활용 기술 문서)의 재료는 개발 과정 기록뿐이다. 게임 내 런타임 AI가 없기 때문이다. 그리고 프롬프트 원문은 시간이 지나면 복원이 불가능하다.
- 프롬프트 원문은 훅이
docs/ai-log/raw/에 자동으로 쌓는다 - ⚠️ 저장소 훅은 각자
/hooks에서 한 번 승인해야 동작한다. 임의 명령을 실행할 수 있어 Claude Code가 승인 전까지 돌리지 않는다. 승인하지 않으면 그 기간 기록이 통째로 빈다 - 원문은 저장소에 올리지 않고 노션에 보관한다. 하루 한 번 수동 업로드.
사유와 파일명 규칙 →
docs/ai-log/raw/README.md - 하루 한 번
/ai-log를 호출해 raw를 정식 엔트리로 승격한다 (Codex에서는$ai-log) - 외부 에셋·오픈소스는 쓰는 순간
docs/ai_usage_log.md§4 출처 표에 적는다. 라이선스를 확인할 수 없는 에셋은 쓰지 않는다 - 작성 규칙 전문 →
docs/ai_log_guidelines.md
훅은 Claude Code와 Codex에서 동작한다. 훅과 스킬의 정본은 .agents/에 한 벌만 두고
.claude/·.codex/는 그것을 가리키기만 한다 — CLAUDE.md가 AGENTS.md를 가리키는 것과 같다.
raw의 각 항목 헤더에 어느 도구로 쓴 것인지가 함께 적힌다. 같은 파일에 둘이 쌓기 때문이고,
그 표기가 docs/ai_log_guidelines.md §3의 도구 필드 근거가 된다.
그 밖의 도구로 작업했다면 그 세션의 원문은 직접 docs/ai-log/raw/에 붙여넣는다.
훅이 없어도 프로세스는 성립해야 한다.
5. 커밋과 브랜치
type(scope): 요약
type—featfixchoredocsrefactorscope— 파일 소유 경계와 같게 쓴다:worldmainphonequizstatecontentbuilddocsai-log- 왜 — 심사 요건이 커밋 기록 유지이고, 8/9에 문서를 쓸 때
git log가 그대로 재료가 된다
본문은 "왜"만 적는다. 무엇을 바꿨는지는 diff가 말한다.
형식은 개요 한두 줄 + 리스트다. 산문 문단을 이어 붙이지 않는다 —
읽는 쪽은 git log에서 훑고, 8/9에 문서를 쓸 때도 훑는다.
type(scope): 요약
왜 필요했는지 한두 줄.
- 판단이 갈린 지점
- 대안을 버린 이유
- 나중에 다시 논의될 것
리스트 개수는 정해 두지 않는다. 다만 한 항목은 한 줄이고, 넘치면 커밋이 두 가지 이유를 담고 있는 것은 아닌지 본다. 적을 게 없으면 제목 한 줄로 끝낸다.
다른 문서나 절을 인용할 때는 그 본문에서 처음 나올 때 파일명을 함께 적는다 — AGENTS.md §3.
이후로는 §3으로 줄여도 된다. 커밋과 PR은 저장소 밖(노션·제출 문서)에서도 읽히므로
§3만 적으면 어느 파일인지 복원할 수 없다. PR 본문에도 같이 적용한다.
규칙
main에 직접 push 하지 않는다. 브랜치 → PR → 병합- squash 병합·rebase·force-push 금지. 커밋 기록이 사라지면 심사 요건 위반이다
(
docs/team-plan.html§05) - 충돌은
git merge main으로 푼다. rebase는--force-with-lease를 붙여도 해시를 갈아끼우고, 훅이 커밋 직후docs/ai-log/raw/에 해시를 박으므로 PR을 열기 전이라도 인용이 끊긴다 - 실제로 8/4에 PR #5를 머지 직전 rebase해 인용 7곳이 죽었다. lease는 정상 동작했다 — lease가 막는 것은 남의 push를 덮어쓰는 사고이지 해시 교체가 아니다
- 브랜치 이름은
feat/…fix/…chore/…docs/…
커밋을 나누고 메시지·PR 본문을 쓰는 일은 /pr-commit(Codex에서는 $pr-commit)이 해준다.
규칙은 여기 있고, 절차는 거기 있다.
develop 브랜치는 쓰지 않는다
feature → PR → main 한 단계로 끝낸다. 근거를 남겨 둔다 — 없으면 누군가 다시 제안한다.
- GitHub Pages는 저장소당 게시 소스가 하나다. develop을 별도 URL로 띄울 수 없으므로 "develop에서 미리 보고 main으로" 흐름이 성립하지 않는다
- 8/7 동결 전까지 main이 깨져도 손해가 없다. 심사는 8/10이다. develop이 지키려는 "항상 배포 가능한 main"의 값이 그때까지 0이다
- 충돌은 이미 파일 소유권(§1)이 막고 있다. develop이 흡수할 충돌 자체가 적다
- 대가는 확실하다 — 머지가 2단계가 되고, 8/7 이후 치명 버그 수정도 develop을 경유한다
8/7 빌드 동결 시점에 태그를 하나 찍어 제출본 기준점을 남긴다. 그날 만든다.
6. 실행과 배포
npx http-server . -p 8080 -c-1 # 저장소 루트에서
- ⚠️
-c-1을 빠뜨리지 말 것.http-server의 기본값이Cache-Control: max-age=3600이라 고친.js가 한 시간 동안 반영되지 않는다. 화면은 옛 코드인데 파일은 새 코드라 "고쳤는데 안 바뀐다"로 시간을 태운다. 더 나쁜 것은 이미 캐시된 뒤에는 서버를-c-1로 다시 띄워도 브라우저가 서버에 묻지 않는다는 것이다 — 그때는 하드 리로드가 필요하다. 8/5에 두 번 걸렸다 file://로 열면 ES 모듈이 CORS로 차단된다. 반드시 정적 서버로 연다- ⚠️ Windows에서
python -m http.server를 쓰지 말 것..js를text/plain으로 내보내 브라우저가 ES 모듈 실행을 거부한다. 로컬에서만 깨지고 Pages에서는 멀쩡해서 원인을 찾기 어렵다 - 배포는 GitHub Pages.
https://busy-man.github.io/busy-man/하위 경로로 서빙되므로 모듈·에셋 경로는 반드시 상대경로(./)로 쓴다. 절대경로(/src/...)는 404다 - 확인은 시크릿 창에서. 로그인 상태에서만 열리는 것을 못 잡는다
Pages 설정
Settings > Pages
Source: Deploy from a branch
Branch: main / (root)
워크플로 파일을 두지 않는다. 배포 파이프라인이 마지막 날 백지를 만드는 실패 모드를
docs/team-plan.html §02가 이미 경고했고, 여기서 실패 지점을 늘릴 이유가 없다.
.nojekyll이 루트에 있어야 한다. 없으면 Jekyll이 .md를 HTML로 변환하고
밑줄로 시작하는 경로를 무시한다.
루트 전체가 웹으로 서빙된다는 점을 알고 있어야 한다. docs/도 prototype/도
URL로 직접 열린다. 이 노출은 검토 후 수용한 것이다 — 저장소가 public이면
어차피 보이는 내용이다. 다만 웹에 올라가면 곤란한 것을 저장소에 넣지 않는다.
7. 하지 말 것
- 번들러·프레임워크·포매터 도입 (남은 시간에 회수되지 않는다)
- 접점 4개 밖의 값을 서로 주고받기
- CDN에서 three.js 불러오기 (
vendor/의 동봉 파일을 상대경로로 import한다) - 상대 담당 파일 수정
- 프로토타입(
prototype/) 수정 — 비교 기준으로 그대로 둔다 - 절대경로로 모듈·에셋 참조
- 커밋 squash, force-push
- 에셋을 넣고 출처 기록을 미루기
코드 스타일
포매터를 두지 않는다. 아래 세 줄이면 충분하다.
- ES 모듈 (
import/export). 세미콜론 유지 - 주석은 한글로 써도 된다. 왜 그렇게 했는지를 적고, 무엇을 하는지는 적지 않는다
- 기존 파일의 스타일을 따른다. 자기 취향으로 남의 코드를 정리하지 않는다
