From 7338cc452c3bef324ac7ca79dc120de0e9a8fb71 Mon Sep 17 00:00:00 2001 From: swrring Date: Fri, 17 Apr 2026 17:17:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=8C=80=20=EC=9E=AC=EB=9F=89=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=9D=BC=EA=B4=84=20+=20=EA=B0=90?= =?UTF-8?q?=EC=82=AC=20=EC=8B=9C=EC=A0=95=20+=20P27-1=20=EA=B0=90=EC=82=AC?= =?UTF-8?q?=EA=B4=80=20=ED=98=B8=EC=B6=9C=20=EC=A3=BC=EC=B2=B4=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## PD님 승인 범위 팀 재량 작업 (2팀 병렬, 일괄 승인 하에 마무리) ### 개발팀 (PD 지시 #1·#5 후속) - Tier 1 잔여 9종 구현: Attribute 3(ReadOnly·ShowIf·ArrayTitle) + Util 6(EnumToInt·EnumEx·FormatEx·MathEx·KeyMaker·ValidationEx) + 테스트 7파일 - Phase 0-C Q-P 응답서 (Q-P1 기획 환송·Q-P2 초벌·시뮬레이터 전략 v2) - 11_UI아키텍처_v1·12_메타시스템_v1 신설 (수상한잡화점 파악 40% 해소) - PD 지시 로그 경로 정규화 (verify_log_paths 18건 전수 통과) ### 기획팀 (기획 #33·#34·#35) - REQ-템플릿_밸런스수치 신설 - 전문가 에이전트 6종(balance/content/level/narrative/system/ux-designer) 기록 의무 명시 + 구 P20 제거 - 밸런싱 md 4종 변경 이력 테이블 표준화(스테이지난이도곡선·밸런싱전략·전체테이블감사·빌드_조건_충돌점검) ## 감사 결과 및 즉시 시정 (PD님 체크 강화 지시 반영) ### dev-auditor 모드 B / plan-auditor 모드 B 수행 - Critical·Major: plan M1(수상한잡화점 대화로그 기획팀 3건 누락) — 즉시 시정 완료 - Minor: dev(Tier 1 엔트리 C30 git 점검 결과 누락) — 즉시 시정 완료 - 감사 보고 2건 `공유/소통/완료/` 이동 ### 프로세스 개선 (P27-1 개정) "감사관 호출 주체 = 항상 상위 세션 PM" 명시화. 근거: Claude Code 서브에이전트는 자기 세션 내부에서 Task 재호출 불가 (양 팀장 실증). 팀장이 감사관 호출 필요 판단 시 PM에게 이관 의무화. ## 조직 기록 체계 정상 작동 확인 - 개발팀 PD 지시 로그·대화로그·소통 채널 4중 동기화 양호 - 기획팀 PD 지시 로그 #33·#34·#35 아카이브 등재, 대화로그 엔트리 append - Inbox 17건 완료/ 이동, 남은 6건은 진행중·상시 참조용 Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/agents/balance-designer.md | 21 +- .claude/agents/content-designer.md | 19 +- .claude/agents/level-designer.md | 19 +- .claude/agents/narrative-designer.md | 20 +- .claude/agents/system-designer.md | 20 +- .claude/agents/ux-designer.md | 19 +- .claude/skills/너드나비스-코어룰/SKILL.md | 7 + scripts/md_to_docx.js | 267 +++++++++++++ 공유/PD_지시_트래킹/개발팀_PD_지시_로그.md | 8 +- 공유/PD_지시_트래킹/기획팀_PD_지시_로그.md | 4 + 공유/대화로그/수상한잡화점/2026-04-17.md | 96 +++++ 공유/대화로그/조직운영/2026-04-17.md | 25 ++ .../2026-04-17_서버_작업_참고자료_v1.2.docx | Bin 0 -> 24558 bytes .../2026-04-17_Phase0-C_QP_응답서_개발팀.md | 125 ++++++ .../개발팀→PM/2026-04-17_서버_작업_참고자료.md | 186 +++++++++ .../2026-04-17_서버개발자_업무지시서_최종본.md | 207 ++++++++++ .../2026-04-17_어뷰징판정_솔루션_기획서_v1.md | 378 ++++++++++++++++++ .../소통/기획팀→개발팀/REQ-템플릿_밸런스수치.md | 142 +++++++ .../2026-04-16_RPT_시뮬레이션_대응_현황보고.md | 0 .../2026-04-16_업무현황_개발실.md | 0 .../2026-04-16_업무현황_기획실.md | 0 .../2026-04-16_콘솔병렬실행_기술검토_개발팀.md | 0 .../2026-04-16_핫리로드대안_기술검토_개발팀.md | 0 .../완료/2026-04-17_RPT_서버역할_정리_초안.md | 315 +++++++++++++++ .../2026-04-17_Unity_MCP_시뮬레이션_기술검토_개발팀.md | 0 .../2026-04-17_Unity_MCP_시뮬레이션_기획검토_기획팀.md | 0 .../완료/2026-04-17_감사보고_재량작업_일괄완료.md | 74 ++++ .../2026-04-17_감사보고_팀기록체계_전수점검.md | 0 .../2026-04-17_업무공유체계_점검_개발팀.md | 0 .../2026-04-17_업무공유체계_점검_기획팀.md | 0 .../2026-04-17_업무공유체계_점검_서버팀.md | 0 .../2026-04-17_업무공유체계_점검_클라이언트팀.md | 0 .../2026-04-17_업무현황_개발팀.md | 0 .../2026-04-17_업무현황_기획팀.md | 0 코어코드/NerdNavis.Framework/CHANGELOG.md | 13 + .../Runtime/Core/Attribute/ArrayTitleAttribute.cs | 54 +++ .../Runtime/Core/Attribute/ReadOnlyAttribute.cs | 27 ++ .../Runtime/Core/Attribute/ShowIfAttribute.cs | 58 +++ .../Runtime/Core/Util/EnumEx.cs | 93 +++++ .../Runtime/Core/Util/EnumToInt.cs | 54 +++ .../Runtime/Core/Util/FormatEx.cs | 96 +++++ .../Runtime/Core/Util/KeyMaker.cs | 81 ++++ .../Runtime/Core/Util/MathEx.cs | 110 +++++ .../Runtime/Core/Util/ValidationEx.cs | 93 +++++ .../Runtime/Core/Attribute/AttributeTests.cs | 70 ++++ .../Tests/Runtime/Core/Util/EnumExTests.cs | 78 ++++ .../Tests/Runtime/Core/Util/EnumToIntTests.cs | 46 +++ .../Tests/Runtime/Core/Util/FormatExTests.cs | 79 ++++ .../Tests/Runtime/Core/Util/KeyMakerTests.cs | 70 ++++ .../Tests/Runtime/Core/Util/MathExTests.cs | 94 +++++ .../Tests/Runtime/Core/Util/ValidationExTests.cs | 85 ++++ .../수상한잡화점/개발/11_UI아키텍처_v1.md | 166 ++++++++ .../수상한잡화점/개발/12_메타시스템_v1.md | 204 ++++++++++ 프로젝트/수상한잡화점/기획/밸런싱전략_v1.md | 11 + .../수상한잡화점/기획/빌드_조건_충돌점검_v1.md | 11 + .../수상한잡화점/기획/스테이지난이도곡선_v1.md | 11 + .../수상한잡화점/기획/전체테이블감사_v1.md | 11 + 57 files changed, 3534 insertions(+), 33 deletions(-) create mode 100644 scripts/md_to_docx.js create mode 100644 공유/서버_작업_참고자료/2026-04-17_서버_작업_참고자료_v1.2.docx create mode 100644 공유/소통/개발팀→PM/2026-04-17_Phase0-C_QP_응답서_개발팀.md create mode 100644 공유/소통/개발팀→PM/2026-04-17_서버_작업_참고자료.md create mode 100644 공유/소통/개발팀→PM/2026-04-17_서버개발자_업무지시서_최종본.md create mode 100644 공유/소통/기획팀→PM/2026-04-17_어뷰징판정_솔루션_기획서_v1.md create mode 100644 공유/소통/기획팀→개발팀/REQ-템플릿_밸런스수치.md rename 공유/소통/{개발팀→PM => 완료}/2026-04-16_RPT_시뮬레이션_대응_현황보고.md (100%) rename 공유/소통/{개발팀→PM => 완료}/2026-04-16_업무현황_개발실.md (100%) rename 공유/소통/{기획팀→PM => 완료}/2026-04-16_업무현황_기획실.md (100%) rename 공유/소통/{개발팀→PM => 완료}/2026-04-16_콘솔병렬실행_기술검토_개발팀.md (100%) rename 공유/소통/{개발팀→PM => 완료}/2026-04-16_핫리로드대안_기술검토_개발팀.md (100%) create mode 100644 공유/소통/완료/2026-04-17_RPT_서버역할_정리_초안.md rename 공유/소통/{개발팀→PM => 완료}/2026-04-17_Unity_MCP_시뮬레이션_기술검토_개발팀.md (100%) rename 공유/소통/{기획팀→PM => 완료}/2026-04-17_Unity_MCP_시뮬레이션_기획검토_기획팀.md (100%) create mode 100644 공유/소통/완료/2026-04-17_감사보고_재량작업_일괄완료.md rename 공유/소통/{pm-auditor→PM => 완료}/2026-04-17_감사보고_팀기록체계_전수점검.md (100%) rename 공유/소통/{개발팀→PM => 완료}/2026-04-17_업무공유체계_점검_개발팀.md (100%) rename 공유/소통/{기획팀→PM => 완료}/2026-04-17_업무공유체계_점검_기획팀.md (100%) rename 공유/소통/{개발팀→PM => 완료}/2026-04-17_업무공유체계_점검_서버팀.md (100%) rename 공유/소통/{개발팀→PM => 완료}/2026-04-17_업무공유체계_점검_클라이언트팀.md (100%) rename 공유/소통/{개발팀→PM => 완료}/2026-04-17_업무현황_개발팀.md (100%) rename 공유/소통/{기획팀→PM => 완료}/2026-04-17_업무현황_기획팀.md (100%) create mode 100644 코어코드/NerdNavis.Framework/Runtime/Core/Attribute/ArrayTitleAttribute.cs create mode 100644 코어코드/NerdNavis.Framework/Runtime/Core/Attribute/ReadOnlyAttribute.cs create mode 100644 코어코드/NerdNavis.Framework/Runtime/Core/Attribute/ShowIfAttribute.cs create mode 100644 코어코드/NerdNavis.Framework/Runtime/Core/Util/EnumEx.cs create mode 100644 코어코드/NerdNavis.Framework/Runtime/Core/Util/EnumToInt.cs create mode 100644 코어코드/NerdNavis.Framework/Runtime/Core/Util/FormatEx.cs create mode 100644 코어코드/NerdNavis.Framework/Runtime/Core/Util/KeyMaker.cs create mode 100644 코어코드/NerdNavis.Framework/Runtime/Core/Util/MathEx.cs create mode 100644 코어코드/NerdNavis.Framework/Runtime/Core/Util/ValidationEx.cs create mode 100644 코어코드/NerdNavis.Framework/Tests/Runtime/Core/Attribute/AttributeTests.cs create mode 100644 코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/EnumExTests.cs create mode 100644 코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/EnumToIntTests.cs create mode 100644 코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/FormatExTests.cs create mode 100644 코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/KeyMakerTests.cs create mode 100644 코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/MathExTests.cs create mode 100644 코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/ValidationExTests.cs create mode 100644 프로젝트/수상한잡화점/개발/11_UI아키텍처_v1.md create mode 100644 프로젝트/수상한잡화점/개발/12_메타시스템_v1.md diff --git a/.claude/agents/balance-designer.md b/.claude/agents/balance-designer.md index 97197ff..f703a80 100644 --- a/.claude/agents/balance-designer.md +++ b/.claude/agents/balance-designer.md @@ -41,11 +41,20 @@ model: sonnet ## 공통 업무 규칙 -> `공유/공통_업무_규칙.md`의 규칙(핵심 규칙 C1~C13 / 프로젝트 규칙 P1~P20)을 준수한다. 핵심 규칙은 위반 불가. -> 특히 **C6 데이터 보호**(수치 밸런스 파일은 변경 전 버전 태그 백업), **C7 재미 우선 원칙**, **P16 산출물 추적성**을 엄수한다. +> `.claude/skills/너드나비스-코어룰/SKILL.md` 단일 SOT(C1~C31 / P1~P27)를 Skill 자동 주입으로 준수한다. 핵심 규칙은 위반 불가. > 팀원은 팀장에게 확인 후 진행하고, 규칙 변경이 필요하면 팀장에게 건의한다. -> **밸런스 특칙**: 수치 테이블(xlsm, csv, json)을 변경할 때는 예외 없이 백업한다. -> -> **C13·P19 PD 지시 트래킹 의무 (헌법급)**: PD님 직접 지시를 인지한 즉시 `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md`에 등록. 시작·진행·완료·중단(사유+사후 조치) 4단계 전부 가시화. 팀장이 등록 못한 경우 팀원이 자체 등록 가능. 누락 시 C3·C13 위반. -> **P20 일일 보고 의무**: 주요 작업 단계 종료 시 `공유/일일보고/YYYY-MM-DD_기획팀.md` 갱신(append). 포함: PD 지시 반영 / 자율 작업 / 발견 이슈 / 다음 예정. + +## 기록 의무 (2026-04-17 개정 — 영역 특화) + +**영역 특화 준수 사항** +- **C6 데이터 보호 (밸런스 파일 변경 전 백업 주체)**: 수치 밸런스 파일(xlsm·csv·json 등) 변경 전 반드시 `{원본명}.bak_YYYYMMDD_HHMM.{확장자}` 형식으로 백업. 밸런스 기획자가 백업 수행 1차 책임자. 백업 없이 편집 시도 금지 (헌법급 위반). +- **C7 재미 우선 원칙**: 모든 수치 변경은 "어떤 재미를 강화하는가"를 먼저 정의. 재미 정의 없는 수치 조정 금지. +- **P16 산출물 추적성**: 밸런스 변경 이력(누가·언제·왜·무엇을·이전값→이후값)을 대상 문서 하단 변경 이력 테이블에 기록. +- **C11 개발 관점 존중**: 개발팀이 자원 효율성·코드 직관성·범용성 관점에서 제기하는 우려를 반드시 협의한다 (C3 은폐 금지). 밸런스 수치 변경 요청 시 `공유/소통/기획팀→개발팀/REQ-템플릿_밸런스수치.md`를 표준으로 사용. + +**공통 기록 의무 (전 에이전트 공통)** +- **C13·P19 PD 지시 트래킹 (헌법급)**: PD님 직접 지시 인지 즉시 `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md` 등록. 4단계(시작·진행·완료·중단) 전부 가시화. 누락 시 C3·C13 위반. +- **P24 대화로그 기록 의무**: 주요 작업·결정·설계 시 `공유/대화로그/{프로젝트}/YYYY-MM-DD.md`에 엔트리 추가. **결정·설계 엔트리는 기각안 필드 필수** (밸런스 수치 결정 시 검토했으나 채택하지 않은 안 + 기각 사유 명시, 공란 금지). 단순 진행 엔트리는 선택. +- **C29-4 완료 후 동기화**: 업무 완료 시 PD 지시 로그 상태 갱신(`완료` + 산출물 경로) + 대화로그 엔트리 + 소통 채널 `완료/` 이동 + Live 더미(`.live/`) 기록을 세트로 수행. +- **plan-auditor 모드 A 권장**: 밸런스 수치 변경·중요 결정 응답 발신 전 `plan-auditor` 감사관 모드 A 호출로 교차 검증(P27-1). diff --git a/.claude/agents/content-designer.md b/.claude/agents/content-designer.md index 9e57990..49519e3 100644 --- a/.claude/agents/content-designer.md +++ b/.claude/agents/content-designer.md @@ -38,10 +38,19 @@ model: sonnet ## 공통 업무 규칙 -> `공유/공통_업무_규칙.md`의 규칙(핵심 규칙 C1~C13 / 프로젝트 규칙 P1~P20)을 준수한다. 핵심 규칙은 위반 불가. -> 특히 **C7 재미 우선 원칙**과 **P16 산출물 추적성**을 엄수한다. +> `.claude/skills/너드나비스-코어룰/SKILL.md` 단일 SOT(C1~C31 / P1~P27)를 Skill 자동 주입으로 준수한다. 핵심 규칙은 위반 불가. > 팀원은 팀장에게 확인 후 진행하고, 규칙 변경이 필요하면 팀장에게 건의한다. -> -> **C13·P19 PD 지시 트래킹 의무 (헌법급)**: PD님 직접 지시를 인지한 즉시 `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md`에 등록. 시작·진행·완료·중단(사유+사후 조치) 4단계 전부 가시화. 팀장이 등록 못한 경우 팀원이 자체 등록 가능. 누락 시 C3·C13 위반. -> **P20 일일 보고 의무**: 주요 작업 단계 종료 시 `공유/일일보고/YYYY-MM-DD_기획팀.md` 갱신(append). 포함: PD 지시 반영 / 자율 작업 / 발견 이슈 / 다음 예정. + +## 기록 의무 (2026-04-17 개정 — 영역 특화) + +**영역 특화 준수 사항** +- **P17 ★ 조건 배타 배치 규칙**: 수상한잡화점 스테이지·빌드 컨텐츠 설계 시 7종 배타 조합 전수 체크. 신규 컨텐츠 조건 추가 시 배타 조합도 함께 정의. 위반 배치는 총괄PM 검증 단계에서 차단된다. +- **P18 설계 문서화 의무**: 신규 컨텐츠 셋·카테고리·스킬 체계 등 설계 결정은 반드시 별도 문서로 명문화. 유령 문서(참조만 남고 본문 부재) 금지. +- **C7 재미 우선 원칙**: 모든 컨텐츠는 "왜 존재하는가"에 답할 수 있어야 한다. 역할 없는 컨텐츠는 버린다. 명확한 상위호환은 기획 실패. + +**공통 기록 의무 (전 에이전트 공통)** +- **C13·P19 PD 지시 트래킹 (헌법급)**: PD님 직접 지시 인지 즉시 `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md` 등록. 4단계(시작·진행·완료·중단) 전부 가시화. 누락 시 C3·C13 위반. +- **P24 대화로그 기록 의무**: 주요 작업·결정·설계 시 `공유/대화로그/{프로젝트}/YYYY-MM-DD.md`에 엔트리 추가. **결정·설계 엔트리는 기각안 필드 필수** (신규 컨텐츠 도입·배제 검토 시 검토했으나 채택하지 않은 안 + 기각 사유 명시, 공란 금지). 단순 진행 엔트리는 선택. +- **C29-4 완료 후 동기화**: 업무 완료 시 PD 지시 로그 상태 갱신(`완료` + 산출물 경로) + 대화로그 엔트리 + 소통 채널 `완료/` 이동 + Live 더미(`.live/`) 기록을 세트로 수행. +- **plan-auditor 모드 A 권장**: 컨텐츠 셋 구조 결정·신규 시스템 컨텐츠 도입 응답 발신 전 `plan-auditor` 감사관 모드 A 호출로 교차 검증(P27-1). diff --git a/.claude/agents/level-designer.md b/.claude/agents/level-designer.md index 4f8ee23..fecd167 100644 --- a/.claude/agents/level-designer.md +++ b/.claude/agents/level-designer.md @@ -34,10 +34,19 @@ model: sonnet ## 공통 업무 규칙 -> `공유/공통_업무_규칙.md`의 규칙(핵심 규칙 C1~C13 / 프로젝트 규칙 P1~P20)을 준수한다. 핵심 규칙은 위반 불가. -> 특히 **C7 재미 우선 원칙**, **P17 ★ 조건 배타 배치 규칙**을 엄수한다 (스테이지·맵 패턴 작업 시 배타 조합 전수 체크). +> `.claude/skills/너드나비스-코어룰/SKILL.md` 단일 SOT(C1~C31 / P1~P27)를 Skill 자동 주입으로 준수한다. 핵심 규칙은 위반 불가. > 팀원은 팀장에게 확인 후 진행하고, 규칙 변경이 필요하면 팀장에게 건의한다. -> -> **C13·P19 PD 지시 트래킹 의무 (헌법급)**: PD님 직접 지시를 인지한 즉시 `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md`에 등록. 시작·진행·완료·중단(사유+사후 조치) 4단계 전부 가시화. 팀장이 등록 못한 경우 팀원이 자체 등록 가능. 누락 시 C3·C13 위반. -> **P20 일일 보고 의무**: 주요 작업 단계 종료 시 `공유/일일보고/YYYY-MM-DD_기획팀.md` 갱신(append). 포함: PD 지시 반영 / 자율 작업 / 발견 이슈 / 다음 예정. + +## 기록 의무 (2026-04-17 개정 — 영역 특화) + +**영역 특화 준수 사항** +- **P17 ★ 조건 배타 배치 규칙**: 스테이지·맵 패턴 작업 시 7종 배타 조합 전수 체크 의무. C9(보스 집중)과 몬스터 등장 패턴 정합성 확인. 위반 배치는 총괄PM 검증 단계에서 차단된다. +- **P18 설계 문서화 의무**: 스테이지 구조·페이싱 곡선·인카운터 배치 설계는 반드시 별도 문서로 명문화. 유령 문서(참조만 남고 본문 부재) 금지. 스테이지 설계 시 결정 배경(왜 이 페이싱인지)·대안·검증 방법을 포함. +- **C7 재미 우선 원칙**: 레벨 목표 한 줄(이 스테이지/맵이 주는 핵심 경험)을 먼저 정의. + +**공통 기록 의무 (전 에이전트 공통)** +- **C13·P19 PD 지시 트래킹 (헌법급)**: PD님 직접 지시 인지 즉시 `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md` 등록. 4단계(시작·진행·완료·중단) 전부 가시화. 누락 시 C3·C13 위반. +- **P24 대화로그 기록 의무**: 주요 작업·결정·설계 시 `공유/대화로그/{프로젝트}/YYYY-MM-DD.md`에 엔트리 추가. **결정·설계 엔트리는 기각안 필드 필수** (스테이지 구조·페이싱 결정 시 검토했으나 채택하지 않은 안 + 기각 사유 명시, 공란 금지). 단순 진행 엔트리는 선택. +- **C29-4 완료 후 동기화**: 업무 완료 시 PD 지시 로그 상태 갱신(`완료` + 산출물 경로) + 대화로그 엔트리 + 소통 채널 `완료/` 이동 + Live 더미(`.live/`) 기록을 세트로 수행. +- **plan-auditor 모드 A 권장**: 스테이지 구조 결정·난이도 곡선 변경 응답 발신 전 `plan-auditor` 감사관 모드 A 호출로 교차 검증(P27-1). diff --git a/.claude/agents/narrative-designer.md b/.claude/agents/narrative-designer.md index 9e17748..4e6ba80 100644 --- a/.claude/agents/narrative-designer.md +++ b/.claude/agents/narrative-designer.md @@ -45,10 +45,20 @@ model: sonnet ## 공통 업무 규칙 -> `공유/공통_업무_규칙.md`의 규칙(핵심 규칙 C1~C13 / 프로젝트 규칙 P1~P20)을 준수한다. 핵심 규칙은 위반 불가. -> 특히 **C7 재미 우선 원칙**을 엄수한다. +> `.claude/skills/너드나비스-코어룰/SKILL.md` 단일 SOT(C1~C31 / P1~P27)를 Skill 자동 주입으로 준수한다. 핵심 규칙은 위반 불가. > 팀원은 팀장에게 확인 후 진행하고, 규칙 변경이 필요하면 팀장에게 건의한다. -> -> **C13·P19 PD 지시 트래킹 의무 (헌법급)**: PD님 직접 지시를 인지한 즉시 `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md`에 등록. 시작·진행·완료·중단(사유+사후 조치) 4단계 전부 가시화. 팀장이 등록 못한 경우 팀원이 자체 등록 가능. 누락 시 C3·C13 위반. -> **P20 일일 보고 의무**: 주요 작업 단계 종료 시 `공유/일일보고/YYYY-MM-DD_기획팀.md` 갱신(append). 포함: PD 지시 반영 / 자율 작업 / 발견 이슈 / 다음 예정. + +## 기록 의무 (2026-04-17 개정 — 영역 특화) + +**영역 특화 준수 사항** +- **P16 산출물 추적성**: 세계관·용어·네이밍·로어 결정의 이력(누가·언제·왜)을 문서에 남긴다. 롤백·회귀 분석 시 활용 가능하도록. 용어 사전(glossary) 유지 책임. +- **P18 설계 문서화 의무**: 세계관·스토리 아웃라인·캐릭터 아크 설계는 반드시 별도 문서로 명문화. 유령 문서 금지. 컷신에만 있는 서사 금지 — 시스템·컨텐츠와 연결 필수. +- **C7 재미 우선 원칙**: 이야기는 플레이어의 선택과 행동을 위한 무대. 소설이 아니다. +- **C22 용어 일관 사용**: 한 번 정한 이름·용어는 영구 유지. 세션마다 다른 이름으로 부르지 말 것 (본 규칙의 실증 사례 존재). + +**공통 기록 의무 (전 에이전트 공통)** +- **C13·P19 PD 지시 트래킹 (헌법급)**: PD님 직접 지시 인지 즉시 `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md` 등록. 4단계(시작·진행·완료·중단) 전부 가시화. 누락 시 C3·C13 위반. +- **P24 대화로그 기록 의무**: 주요 작업·결정·설계 시 `공유/대화로그/{프로젝트}/YYYY-MM-DD.md`에 엔트리 추가. **결정·설계 엔트리는 기각안 필드 필수** (세계관·네이밍·스토리 아크 결정 시 검토했으나 채택하지 않은 안 + 기각 사유 명시, 공란 금지). 단순 진행 엔트리는 선택. +- **C29-4 완료 후 동기화**: 업무 완료 시 PD 지시 로그 상태 갱신(`완료` + 산출물 경로) + 대화로그 엔트리 + 소통 채널 `완료/` 이동 + Live 더미(`.live/`) 기록을 세트로 수행. +- **plan-auditor 모드 A 권장**: 세계관 핵심 결정·네이밍 체계 확정 응답 발신 전 `plan-auditor` 감사관 모드 A 호출로 교차 검증(P27-1). diff --git a/.claude/agents/system-designer.md b/.claude/agents/system-designer.md index 74c9925..46f798c 100644 --- a/.claude/agents/system-designer.md +++ b/.claude/agents/system-designer.md @@ -33,10 +33,20 @@ model: sonnet ## 공통 업무 규칙 -> `공유/공통_업무_규칙.md`의 규칙(핵심 규칙 C1~C13 / 프로젝트 규칙 P1~P20)을 준수한다. 핵심 규칙은 위반 불가. -> 특히 **C7 재미 우선 원칙**, **C3 이슈 은폐 금지·즉시 보고**, **P17 ★ 조건 배타 배치 규칙**을 엄수한다. +> `.claude/skills/너드나비스-코어룰/SKILL.md` 단일 SOT(C1~C31 / P1~P27)를 Skill 자동 주입으로 준수한다. 핵심 규칙은 위반 불가. > 팀원은 팀장에게 확인 후 진행하고, 규칙 변경이 필요하면 팀장에게 건의한다. -> -> **C13·P19 PD 지시 트래킹 의무 (헌법급)**: PD님 직접 지시를 인지한 즉시 `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md`에 등록. 시작·진행·완료·중단(사유+사후 조치) 4단계 전부 가시화. 팀장이 등록 못한 경우 팀원이 자체 등록 가능. 누락 시 C3·C13 위반. -> **P20 일일 보고 의무**: 주요 작업 단계 종료 시 `공유/일일보고/YYYY-MM-DD_기획팀.md` 갱신(append). 포함: PD 지시 반영 / 자율 작업 / 발견 이슈 / 다음 예정. + +## 기록 의무 (2026-04-17 개정 — 영역 특화) + +**영역 특화 준수 사항** +- **P18 설계 문서화 의무 (시스템 설계 1차 대상)**: 핵심 게임 루프·메카닉·시스템 규칙은 반드시 별도 설계 문서로 명문화. 유령 문서(참조만 남고 본문 부재) 금지. 설계 변경·대체(코어 교체 등) 시 신규 설계안 문서 필수, 기존 문서는 "대체됨" 표시 후 보관. 필수 포함: 결정 배경·선택 방향과 대안(trade-off)·구현 가이드라인·검증 방법·변경 이력. +- **C3 이슈 은폐 금지**: 시스템 간 충돌·논리 모순 발견 즉시 팀장에게 보고. +- **C7 재미 우선 원칙**: 이 시스템이 풀고자 하는 플레이어 문제·제공하는 경험을 먼저 정의. "재미있게 전투한다" 같은 추상 표현 금지. +- **P17 ★ 조건 배타 배치 규칙**: 시스템 신규 규칙 도입 시 기존 컨텐츠·레벨 설계와 배타 조합 정합성 확인. + +**공통 기록 의무 (전 에이전트 공통)** +- **C13·P19 PD 지시 트래킹 (헌법급)**: PD님 직접 지시 인지 즉시 `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md` 등록. 4단계(시작·진행·완료·중단) 전부 가시화. 누락 시 C3·C13 위반. +- **P24 대화로그 기록 의무**: 주요 작업·결정·설계 시 `공유/대화로그/{프로젝트}/YYYY-MM-DD.md`에 엔트리 추가. **결정·설계 엔트리는 기각안 필드 필수** (시스템 구조·메카닉 결정 시 검토했으나 채택하지 않은 안 + 기각 사유 명시, 공란 금지). 단순 진행 엔트리는 선택. +- **C29-4 완료 후 동기화**: 업무 완료 시 PD 지시 로그 상태 갱신(`완료` + 산출물 경로) + 대화로그 엔트리 + 소통 채널 `완료/` 이동 + Live 더미(`.live/`) 기록을 세트로 수행. +- **plan-auditor 모드 A 권장**: 시스템 설계 확정·메카닉 변경 응답 발신 전 `plan-auditor` 감사관 모드 A 호출로 교차 검증(P27-1). diff --git a/.claude/agents/ux-designer.md b/.claude/agents/ux-designer.md index b03fdc4..f03a3e0 100644 --- a/.claude/agents/ux-designer.md +++ b/.claude/agents/ux-designer.md @@ -43,10 +43,19 @@ model: sonnet ## 공통 업무 규칙 -> `공유/공통_업무_규칙.md`의 규칙(핵심 규칙 C1~C13 / 프로젝트 규칙 P1~P20)을 준수한다. 핵심 규칙은 위반 불가. -> 특히 **C7 재미 우선 원칙**을 엄수한다. +> `.claude/skills/너드나비스-코어룰/SKILL.md` 단일 SOT(C1~C31 / P1~P27)를 Skill 자동 주입으로 준수한다. 핵심 규칙은 위반 불가. > 팀원은 팀장에게 확인 후 진행하고, 규칙 변경이 필요하면 팀장에게 건의한다. -> -> **C13·P19 PD 지시 트래킹 의무 (헌법급)**: PD님 직접 지시를 인지한 즉시 `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md`에 등록. 시작·진행·완료·중단(사유+사후 조치) 4단계 전부 가시화. 팀장이 등록 못한 경우 팀원이 자체 등록 가능. 누락 시 C3·C13 위반. -> **P20 일일 보고 의무**: 주요 작업 단계 종료 시 `공유/일일보고/YYYY-MM-DD_기획팀.md` 갱신(append). 포함: PD 지시 반영 / 자율 작업 / 발견 이슈 / 다음 예정. + +## 기록 의무 (2026-04-17 개정 — 영역 특화) + +**영역 특화 준수 사항** +- **P18 설계 문서화 의무 (UI/UX 플로우 1차 대상)**: 화면 플로우·정보 구조·조작 스킴 설계는 반드시 별도 문서로 명문화. 유령 문서(참조만 남고 본문 부재) 금지. 설계 변경 시 신규 문서 + 기존 "대체됨" 표시. 플로우 다이어그램·화면별 정보 우선순위·예외 경로를 포함. +- **C7 재미 우선 원칙**: UI가 플레이어에게 주는 경험(즉각적 피드백·한 화면 한 결정)을 명확히 정의. +- **접근성 기본 원칙**: 접근성은 추가가 아니라 기본. 처음부터 고려. + +**공통 기록 의무 (전 에이전트 공통)** +- **C13·P19 PD 지시 트래킹 (헌법급)**: PD님 직접 지시 인지 즉시 `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md` 등록. 4단계(시작·진행·완료·중단) 전부 가시화. 누락 시 C3·C13 위반. +- **P24 대화로그 기록 의무**: 주요 작업·결정·설계 시 `공유/대화로그/{프로젝트}/YYYY-MM-DD.md`에 엔트리 추가. **결정·설계 엔트리는 기각안 필드 필수** (화면 구조·플로우 결정 시 검토했으나 채택하지 않은 안 + 기각 사유 명시, 공란 금지). 단순 진행 엔트리는 선택. +- **C29-4 완료 후 동기화**: 업무 완료 시 PD 지시 로그 상태 갱신(`완료` + 산출물 경로) + 대화로그 엔트리 + 소통 채널 `완료/` 이동 + Live 더미(`.live/`) 기록을 세트로 수행. +- **plan-auditor 모드 A 권장**: UI/UX 구조 확정·신규 화면 도입 응답 발신 전 `plan-auditor` 감사관 모드 A 호출로 교차 검증(P27-1). diff --git a/.claude/skills/너드나비스-코어룰/SKILL.md b/.claude/skills/너드나비스-코어룰/SKILL.md index e6374a3..6a5835b 100644 --- a/.claude/skills/너드나비스-코어룰/SKILL.md +++ b/.claude/skills/너드나비스-코어룰/SKILL.md @@ -943,6 +943,13 @@ grep -r "기각안" 공유/대화로그/ # 기각 이유 추적 - 세션 말미에 **모드 B 주기 감사** 최소 1회 수행 - 3개 감사관은 상호 교차 검증 가능 (dev-auditor 결과를 pm-auditor가 메타 검증 등) +**감사관 호출 주체 (2026-04-17 추가 — 구조적 제약 명시화)**: +- Claude Code 서브에이전트는 **자기 세션 내부에서 Task 도구로 타 Agent를 재호출할 수 없다** (환경 제약) +- 따라서 **감사관(pm/dev/plan-auditor) 호출 주체는 항상 상위 세션의 PM**이다 +- 개발팀장·기획팀장이 "dev/plan-auditor 모드 A·B 호출이 필요하다"고 판단한 경우, **호출 요청을 PM에게 이관**하여 PM이 대리 수행 (2026-04-17 개발팀장·기획팀장 양측 실증으로 구조 확인) +- PM은 팀장급의 감사관 호출 요청을 **선택 아닌 의무**로 처리 (C29 자율 수행 원칙상 팀장이 판단한 필요는 존중) +- 본 제약은 Claude Code의 환경 제약에 종속되므로 향후 환경 변경 시 본 조항 재검토 + ### P27-2. Agent 호출 이력 기록 의무 (신설 / 2026-04-17 호출 프롬프트 3요소 추가) PM 또는 어떤 에이전트가 Agent 도구로 다른 에이전트를 호출할 때: diff --git a/scripts/md_to_docx.js b/scripts/md_to_docx.js new file mode 100644 index 0000000..6dadd01 --- /dev/null +++ b/scripts/md_to_docx.js @@ -0,0 +1,267 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); +const { + Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, + HeadingLevel, AlignmentType, BorderStyle, WidthType, ShadingType, + LevelFormat, TableOfContents, PageBreak, TabStopType, TabStopPosition +} = require('docx'); + +const srcPath = process.argv[2]; +const dstPath = process.argv[3]; +if (!srcPath || !dstPath) { console.error('usage: md_to_docx.js '); process.exit(1); } + +const FONT = 'Malgun Gothic'; +const raw = fs.readFileSync(srcPath, 'utf8'); + +// Strip YAML frontmatter +let md = raw; +if (md.startsWith('---')) { + const end = md.indexOf('\n---', 3); + if (end > 0) md = md.slice(end + 4).replace(/^\s*\n/, ''); +} + +const lines = md.split(/\r?\n/); + +const border = { style: BorderStyle.SINGLE, size: 6, color: 'CCCCCC' }; +const cellBorders = { top: border, bottom: border, left: border, right: border }; + +function run(text, opts = {}) { + return new TextRun({ text, font: FONT, size: opts.size || 22, bold: !!opts.bold, italics: !!opts.italic }); +} + +// Parse inline **bold**, *italic*, `code` -> TextRun[] +function parseInline(text, baseOpts = {}) { + const runs = []; + const re = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g; + let last = 0; let m; + while ((m = re.exec(text)) !== null) { + if (m.index > last) runs.push(run(text.slice(last, m.index), baseOpts)); + const tok = m[0]; + if (tok.startsWith('**')) runs.push(run(tok.slice(2, -2), { ...baseOpts, bold: true })); + else if (tok.startsWith('`')) runs.push(new TextRun({ text: tok.slice(1, -1), font: 'Consolas', size: baseOpts.size || 22 })); + else runs.push(run(tok.slice(1, -1), { ...baseOpts, italic: true })); + last = m.index + tok.length; + } + if (last < text.length) runs.push(run(text.slice(last), baseOpts)); + if (runs.length === 0) runs.push(run(text, baseOpts)); + return runs; +} + +function para(text, opts = {}) { + return new Paragraph({ + children: parseInline(text, opts), + spacing: { before: 60, after: 60 }, + ...(opts.heading ? { heading: opts.heading } : {}), + }); +} + +function heading(text, level) { + const map = { 1: HeadingLevel.HEADING_1, 2: HeadingLevel.HEADING_2, 3: HeadingLevel.HEADING_3, 4: HeadingLevel.HEADING_4 }; + const size = { 1: 36, 2: 30, 3: 26, 4: 24 }[level] || 22; + return new Paragraph({ + heading: map[level] || HeadingLevel.HEADING_4, + children: [new TextRun({ text, font: FONT, size, bold: true })], + spacing: { before: 240, after: 120 }, + }); +} + +function bullet(text, level = 0) { + return new Paragraph({ + numbering: { reference: 'bullets', level }, + children: parseInline(text), + spacing: { before: 40, after: 40 }, + }); +} + +function numbered(text, level = 0) { + return new Paragraph({ + numbering: { reference: 'numbers', level }, + children: parseInline(text), + spacing: { before: 40, after: 40 }, + }); +} + +function codeBlock(text) { + return new Paragraph({ + children: [new TextRun({ text, font: 'Consolas', size: 20 })], + shading: { type: ShadingType.CLEAR, fill: 'F4F4F4' }, + spacing: { before: 60, after: 60 }, + }); +} + +function quote(text) { + return new Paragraph({ + children: parseInline(text, { italic: true }), + indent: { left: 360 }, + spacing: { before: 60, after: 60 }, + border: { left: { style: BorderStyle.SINGLE, size: 18, color: '2E75B6', space: 12 } }, + }); +} + +// Parse pipe table starting at index i, returns { table, nextIndex } +function parseTable(startIdx) { + const rows = []; + let i = startIdx; + while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i])) { + rows.push(lines[i].trim()); + i++; + } + if (rows.length < 2) return null; + // Header | separator | body + const split = (r) => r.slice(1, -1).split('|').map(c => c.trim()); + const header = split(rows[0]); + const body = rows.slice(2).map(split); + const colCount = header.length; + const totalWidth = 9000; + const colWidth = Math.floor(totalWidth / colCount); + const columnWidths = new Array(colCount).fill(colWidth); + + const makeCell = (txt, isHeader) => new TableCell({ + borders: cellBorders, + width: { size: colWidth, type: WidthType.DXA }, + shading: isHeader ? { type: ShadingType.CLEAR, fill: 'D5E8F0' } : undefined, + margins: { top: 80, bottom: 80, left: 120, right: 120 }, + children: [new Paragraph({ children: parseInline(txt, { bold: isHeader, size: 20 }) })], + }); + + const tableRows = [ + new TableRow({ children: header.map(h => makeCell(h, true)) }), + ...body.map(r => new TableRow({ children: r.concat(new Array(Math.max(0, colCount - r.length)).fill('')).slice(0, colCount).map(c => makeCell(c, false)) })) + ]; + return { + table: new Table({ width: { size: totalWidth, type: WidthType.DXA }, columnWidths, rows: tableRows }), + nextIndex: i, + }; +} + +const children = []; +// Cover +children.push(new Paragraph({ + children: [new TextRun({ text: '인간 서버 개발자 업무 지시서', font: FONT, size: 44, bold: true })], + alignment: AlignmentType.CENTER, + spacing: { before: 400, after: 200 }, +})); +children.push(new Paragraph({ + children: [new TextRun({ text: '수상한잡화점 서버 파트 — v1.0', font: FONT, size: 28 })], + alignment: AlignmentType.CENTER, + spacing: { after: 120 }, +})); +children.push(new Paragraph({ + children: [new TextRun({ text: '발행: 개발팀장 · 수신: 인간 서버 개발자 · 일자: 2026-04-17', font: FONT, size: 22, italics: true })], + alignment: AlignmentType.CENTER, + spacing: { after: 600 }, +})); +children.push(new Paragraph({ children: [new PageBreak()] })); + +// Manual index (no TOC field, avoids Word's external-reference warning) +children.push(new Paragraph({ + children: [new TextRun({ text: '목차', font: FONT, size: 32, bold: true })], + spacing: { before: 120, after: 240 }, +})); +for (const L of lines) { + const hm = /^(#{1,3})\s+(.*)$/.exec(L); + if (!hm) continue; + const lv = hm[1].length; + const indent = (lv - 1) * 360; + children.push(new Paragraph({ + children: [new TextRun({ text: hm[2], font: FONT, size: 22 })], + indent: { left: indent }, + spacing: { before: 20, after: 20 }, + })); +} +children.push(new Paragraph({ children: [new PageBreak()] })); + +let i = 0; +while (i < lines.length) { + const line = lines[i]; + // Table + if (/^\s*\|.*\|\s*$/.test(line) && i + 1 < lines.length && /^\s*\|[\s:\-|]+\|\s*$/.test(lines[i + 1])) { + const result = parseTable(i); + if (result) { children.push(result.table); children.push(new Paragraph({ children: [new TextRun('')] })); i = result.nextIndex; continue; } + } + // Heading + const hMatch = /^(#{1,6})\s+(.*)$/.exec(line); + if (hMatch) { children.push(heading(hMatch[2], Math.min(hMatch[1].length, 4))); i++; continue; } + // Code block + if (/^```/.test(line)) { + const buf = []; + i++; + while (i < lines.length && !/^```/.test(lines[i])) { buf.push(lines[i]); i++; } + i++; + if (buf.length) children.push(codeBlock(buf.join('\n'))); + continue; + } + // Quote + if (/^>\s?/.test(line)) { + children.push(quote(line.replace(/^>\s?/, ''))); + i++; continue; + } + // Horizontal rule + if (/^---+\s*$/.test(line)) { + children.push(new Paragraph({ border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: '999999', space: 1 } }, spacing: { before: 120, after: 120 } })); + i++; continue; + } + // Bullet + const bulletMatch = /^(\s*)[-*]\s+(.*)$/.exec(line); + if (bulletMatch) { + const level = Math.min(Math.floor(bulletMatch[1].length / 2), 3); + children.push(bullet(bulletMatch[2], level)); + i++; continue; + } + // Numbered + const numMatch = /^(\s*)\d+\.\s+(.*)$/.exec(line); + if (numMatch) { + const level = Math.min(Math.floor(numMatch[1].length / 2), 3); + children.push(numbered(numMatch[2], level)); + i++; continue; + } + // Empty + if (/^\s*$/.test(line)) { i++; continue; } + // Paragraph + children.push(para(line)); + i++; +} + +const doc = new Document({ + creator: '너드나비스 개발팀', + title: '인간 서버 개발자 업무 지시서 — 수상한잡화점', + styles: { + default: { document: { run: { font: FONT, size: 22 } } }, + paragraphStyles: [ + { id: 'Heading1', name: 'Heading 1', basedOn: 'Normal', next: 'Normal', quickFormat: true, + run: { size: 36, bold: true, font: FONT, color: '1F3864' }, + paragraph: { spacing: { before: 360, after: 180 }, outlineLevel: 0 } }, + { id: 'Heading2', name: 'Heading 2', basedOn: 'Normal', next: 'Normal', quickFormat: true, + run: { size: 30, bold: true, font: FONT, color: '2E75B6' }, + paragraph: { spacing: { before: 280, after: 140 }, outlineLevel: 1 } }, + { id: 'Heading3', name: 'Heading 3', basedOn: 'Normal', next: 'Normal', quickFormat: true, + run: { size: 26, bold: true, font: FONT }, + paragraph: { spacing: { before: 200, after: 100 }, outlineLevel: 2 } }, + { id: 'Heading4', name: 'Heading 4', basedOn: 'Normal', next: 'Normal', quickFormat: true, + run: { size: 24, bold: true, font: FONT }, + paragraph: { spacing: { before: 160, after: 80 }, outlineLevel: 3 } }, + ], + }, + numbering: { + config: [ + { reference: 'bullets', levels: [0, 1, 2, 3].map(lv => ({ level: lv, format: LevelFormat.BULLET, text: ['•','◦','▪','▫'][lv], alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 720 * (lv + 1), hanging: 360 } } } })) }, + { reference: 'numbers', levels: [0, 1, 2, 3].map(lv => ({ level: lv, format: LevelFormat.DECIMAL, text: `%${lv+1}.`, alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 720 * (lv + 1), hanging: 360 } } } })) }, + ], + }, + sections: [{ + properties: { + page: { + size: { width: 11906, height: 16838 }, // A4 + margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, + }, + }, + children, + }], +}); + +Packer.toBuffer(doc).then(buf => { + fs.mkdirSync(path.dirname(dstPath), { recursive: true }); + fs.writeFileSync(dstPath, buf); + console.log('OK', dstPath, buf.length, 'bytes'); +}); diff --git a/공유/PD_지시_트래킹/개발팀_PD_지시_로그.md b/공유/PD_지시_트래킹/개발팀_PD_지시_로그.md index 5bd2188..eca2388 100644 --- a/공유/PD_지시_트래킹/개발팀_PD_지시_로그.md +++ b/공유/PD_지시_트래킹/개발팀_PD_지시_로그.md @@ -32,9 +32,9 @@ C3·C13 위반에 해당. **즉시 자진 보고 후 소급 등록**. | # | 일시 | 지시 요지 | 처리 상태 | 산출물 경로 | 중단 사유 | 사후 조치 | |---|------|----------|----------|-----------|----------|----------| | 28 | 2026-04-16 | (PD님 직접 지시, 총괄PM 경유) 기획팀 밸런스 작업을 위한 시뮬레이션 대응 — 07 착수계획(시뮬레이터 이원화 해소) 진행 상태 보고 + 기획팀이 밸런스 작업에 사용할 수 있는 시뮬레이션 환경 구축. **2026-04-17 PD님 직접 지시로 Unity MCP 기반 시뮬레이션 방향으로 전환** (커밋 `db64310`). | 진행중 | `공유/소통/PM→개발팀/2026-04-16_REQ_시뮬레이션_대응_기획팀_밸런스작업_지원.md`, `공유/소통/개발팀→PM/2026-04-16_RPT_시뮬레이션_대응_현황보고.md`, **`공유/소통/개발팀→PM/2026-04-17_Unity_MCP_시뮬레이션_기술검토_개발팀.md` (2026-04-17 Unity MCP 방향 기술검토 완료)** | - | **2026-04-17 PD님 해석 확정**: 헌법 제1원칙 목표 2 = "코어 코드 프레임워크 차기 프로젝트 활용" 의미. 차기 프로젝트 Unity 전제 여부는 본 목표와 무관 → 개발팀장 상신 사항 해소. **Python 시뮬 파일 소실 확정 — 폐기 사안, 교차 검증 축 단일(Unity MCP)로 확정**. Phase 3 재개 로드맵은 PD님 별도 논의 예정 | 07_v2·11·12 신설 착수는 PD님 Phase 3 재개 논의 후 재개 (PM 재량 대기) | -| 1 | 2026-04-14 | NerdNavisCore 타 회사 소유 전환·담당자 퇴사 사실 통보, 자체 범용 코어 신규 제작 결정 | 진행중 | `프로젝트/수상한잡화점/개발/06_신규코어_설계안_v1.md` (초안, 2026-04-16 재구조 시 경로 이관) / **구 `개발팀/코어_설계/` 흡수 완료 판정 (2026-04-17 개발팀장 실측, pm-auditor Critical C1 해소)**: ①`01_아키텍처_개요_v1.md` → `프로젝트/코어프레임워크/01_아키텍처_개요_v1.md` (커밋 `1f50ce5` 이관), ②`02_수상한잡화점_추출대상_v1.md` → `프로젝트/코어프레임워크/02_수상한잡화점_추출대상_v1.md` (동 커밋 이관), ③`_skeleton/` → `코어코드/NerdNavis.Framework/Runtime·Editor/` (커밋 `7187ac6` 코어코드 레포 통합으로 실체 구현 발전적 흡수) / **`코어코드/NerdNavis.Framework/Runtime/Core/` 구현체 — Tier 1 기반 Core 4종 완료 (Log·CoroutineRunner·MonoSingleton·ServiceLocator + 테스트 28건) + `코어코드/NerdNavis.Framework/` git 통합 완료 (커밋 `7187ac6`)** | - | OI-1(네임스페이스 NerdNavis.*) PD님 확정 반영 완료. **OI-2 C+H1 PD님 승인 완료** (2026-04-16 교차 검증으로 확인, 커밋 `7187ac6` 메시지에 명시). OI-3 불요 결정, OI-4 A안 확정, OI-5 폐기. Tier 1+2 MVP 범위 PD님 확정 반영. **Tier 1 잔여 9종 모듈 구현 미착수** (차단 요인 없음, 즉시 착수 가능) | +| 1 | 2026-04-14 | NerdNavisCore 타 회사 소유 전환·담당자 퇴사 사실 통보, 자체 범용 코어 신규 제작 결정 | 진행중 | `프로젝트/수상한잡화점/개발/06_신규코어_설계안_v1.md` / `프로젝트/코어프레임워크/01_아키텍처_개요_v1.md` / `프로젝트/코어프레임워크/02_수상한잡화점_추출대상_v1.md` / `프로젝트/코어프레임워크/03_배포방식_안건_v1.md` / `코어코드/NerdNavis.Framework/` (Tier 1 Core 4종 + **Attribute 3종 + Util 6종 = 신규 9종 구현 완료 2026-04-17**: ReadOnly/ShowIf/ArrayTitle 3종 + EnumToInt/EnumEx/FormatEx/MathEx/KeyMaker/ValidationEx 6종 + 각 단위 테스트) | - | **Tier 1 잔여 구현 부분 완료 (2026-04-17 마무리 지시)**. 16종 중 13종 완료, 잔여 Data/Event/Container 등은 상호작용 설계 재검증 필요. 경로 정규화 완료로 `verify_log_paths.sh` 감사 15건 전수 실존 확인 통과. OI-1·OI-2 C+H1·OI-3·OI-4 확정, OI-5 폐기 | | 2 | 2026-04-14 | 서버 Critical 보안 3건 보류 | 보류 | `프로젝트/수상한잡화점/개발/05_서버연동_현황_v1.md` (2026-04-16 재구조 시 경로 이관, 2026-04-17 pm-auditor 감사 Critical C1 정정) | 서버 파트 정비 미완료 (PD님 지시) | 서버팀 가동 시점에 블로커급 재개. 담당: 서버팀장. 재개 트리거: 서버 파트 정비 완료 통보 | -| 5 | 2026-04-15 | (3대 지시) **A.** Framework Tier 1 기반 Core 모듈 구현 착수 (Logger·ServiceLocator·CoroutineRunner 등 — 설계 문서 재확인 후 파일 단위). **B.** 수상한 잡화점 Phase 0-B/C 재개. **C.** 위 내용을 총괄PM에게도 보고 | 진행중 | **A 완료분**: `코어코드/NerdNavis.Framework/Runtime/Core/**` (Log·CoroutineRunner·MonoSingleton·ServiceLocator 4종 + 테스트 28건). / **B-1/B-2/B-3 완료**: `프로젝트/수상한잡화점/개발/08_전투시스템_SOT_v1.md`, `09_카드시스템_아키텍처_v1.md`, `10_데이터로딩_구조_v1.md`. / **C 일괄 공유 완료** | - | Tier 1 잔여 9종 미구현 + Phase 0-C(Q-P1/P2/P3 응답서·시뮬레이터 전략) 미착수. 시뮬레이션 트랙은 2026-04-17 PD님 지시로 Unity MCP 활용 방향으로 전환(#28에서 통합 관리). | +| 5 | 2026-04-15 | (3대 지시) **A.** Framework Tier 1 기반 Core 모듈 구현 착수 (Logger·ServiceLocator·CoroutineRunner 등 — 설계 문서 재확인 후 파일 단위). **B.** 수상한 잡화점 Phase 0-B/C 재개. **C.** 위 내용을 총괄PM에게도 보고 | 진행중 | **A 부분 완료** (#1 항목 참조 — 2026-04-17 Attribute 3종 + Util 6종 추가 구현). **B-1/B-2/B-3 완료**: `프로젝트/수상한잡화점/개발/08_전투시스템_SOT_v1.md` · `프로젝트/수상한잡화점/개발/09_카드시스템_아키텍처_v1.md` · `프로젝트/수상한잡화점/개발/10_데이터로딩_구조_v1.md`. **B-4/B-5 완료 2026-04-17**: `프로젝트/수상한잡화점/개발/11_UI아키텍처_v1.md` · `프로젝트/수상한잡화점/개발/12_메타시스템_v1.md`. **C-Phase0-C 부분 완료 2026-04-17**: `공유/소통/개발팀→PM/2026-04-17_Phase0-C_QP_응답서_개발팀.md` (Q-P1 기획 의도 영역 환송 + Q-P2 초벌 스캔 + 시뮬레이터 전략 v2 Unity MCP 단일축 재설정). **C 일괄 공유 완료** | - | Q-P2 정밀 2차 응답 + Unity MCP 시뮬레이션 인프라 4종 (SimulationRunner 프로토타입·파라미터 외부화·결과 JSON 스키마·MCP 호출 스니펫) 구현은 후속 작업 | > **2026-04-15 오후 추가 갱신 (C4·C13 위반 자진 정정 2차)**: > #5번 신규 등재. PD님 3대 지시(A/B/C) 및 #1 산출물 경로에 Framework Tier 1 구현체(`D:/NerdNavis/NerdNavis.Framework/`)를 소급 등록. **B 착수 시점 및 Git 동기화 병렬 지시(#4) 착수 시점에 총괄PM 공유를 누락**한 건을 PD님이 직접 지적하여 즉시 정정. 근본 원인: "C 항목 진행 전 지시 대기" 지시를 본인이 **PM 공유 전체 보류**로 잘못 확대 해석. C4(총괄PM 하달)·C13(4단계 가시화)의 "작업 착수 시점=상시 공유 의무" 원칙을 거스른 것. 재발 방지 관례: **신규 트랙 착수 즉시 pm-general 공유 → TodoWrite 항목 생성** (총괄PM 채택 권고). 자체 경위는 `공유/일일보고/2026-04-15_개발팀.md` 오후 섹션 참조. @@ -87,6 +87,10 @@ C3·C13 위반에 해당. **즉시 자진 보고 후 소급 등록**. | # | 일시 | 지시 요지 | 처리 상태 | 산출물 경로 | 중단 사유 | 사후 조치 | |---|------|----------|----------|-----------|----------|----------| +| 32 | 2026-04-17 | (PD님 직접 지시, PM 경유) **서버 작업 참고 자료 v1.2 재작성 (외부 서버 작업자용 중립화)** — (1) PlayFab 전제 제거(현 사용 중 상태로만 중립 기술, 스택 선택은 열린 결정 사항), (2) 조직 내부 프로세스 내용 전면 제거(코어룰 참조·PD 지시 번호·결정 대기 섹션·frontmatter related/depends_on·기각안 섹션 삭제), (3) 문서 성격 재정의("인간 서버 개발자 업무 지시서" → "서버 작업 참고 자료", 지시형 → 제공형 톤). 신규 파일로 분리 작성, v1.1은 조직 내부용 상세본으로 보존 | **완료** | `공유/소통/개발팀→PM/2026-04-17_서버_작업_참고자료.md` (v1.2, 외부 서버 작업자용). frontmatter: type: 참고자료, audience: 외부 서버 작업자. "인간 서버 개발자"·"인간 작업자" 단어 전부 제거. v1.1(조직 내부용 상세본)은 원 경로 유지 | - | DOCX 변환은 PM이 `anthropic-skills:docx`로 재생성. 외부 전달 시 v1.2 사용, 조직 내부 참조는 v1.1 상세본 유지 | +| 31 | 2026-04-17 | (PD님 직접 지시, PM 경유) **서버 개발자 지시서 v1.1 요약판 재작성** — (1) 어뷰징 판정 책임 클라 100% 재확정 (서버는 `is_abuse_flag` 수신만, 경계값 보관·검증 안 함), (2) 인간 개발자 5~7분 완독 가능한 요약판으로 v1.0(446줄) 전면 재작성. 개발팀장 수행 | **완료** | `공유/소통/개발팀→PM/2026-04-17_서버개발자_업무지시서_최종본.md` (v1.1, 207줄). 섹션 5 "어뷰징 방지 — 클라 주도, 서버 최소 역할"로 정정. 샘플 API 1건(`Save_StageResult`) 유지, 템플릿·매트릭스 세부 제거. 기각안 5종 명시(v1.0 B-7 구조 폐기 포함) | - | DOCX는 PM이 `anthropic-skills:docx`로 재생성. 후속 PD-③·PD-④는 인간 개발자 배정 후 수렴 | +| 30 | 2026-04-17 | (PD님 직접 지시, PM 경유) **인간 서버 개발자 업무 지시서 최종본 작성** — PD 확정 3건 반영(보상 재화 통일·어뷰징 방지 기획팀 주도·DOCX 변환 제작). 개발팀장 최종본 md 작성, DOCX 변환은 PM 수행 | **완료 (v1.1로 대체)** | `공유/소통/개발팀→PM/2026-04-17_서버개발자_업무지시서_최종본.md` (v1.0 → v1.1로 재작성, 2026-04-17 PD님 재결정). B-7 서버 경계값 검증 구조는 #31에서 폐기 | - | v1.0 상태는 #31(v1.1 재작성) 기각안 섹션에 영구 보존. 본 항목은 초판 작성 완료로 마감 | +| 29 | 2026-04-17 | (PD님 직접 지시, PM 경유) 인간 서버 개발자 업무 지시서 **초안** 작성 — 개발팀이 PM과 상의해서 서버 역할·클라/서버 경계·PD 결정 안건을 정리해 보고 | **완료** (최종본 #30으로 승격) | `공유/소통/완료/2026-04-17_RPT_서버역할_정리_초안.md` (초안 아카이브 이관). 현 수상한잡화점 `ServerClass.cs`·`ServerInfo.cs` 실측 + PD 결정 안건 5건 초안 제시 | - | PD 확정 3건(보상 재화 통일·어뷰징 방지 기획팀 주도·DOCX 제작) 수령 후 #30 최종본으로 승격 갱신. 본 항목은 초안 작성 완료로 마감 | | 17 | 2026-04-15 | (PD님 직접 승인) **C17-3 보강 + 진입 절차 3요소 의무 + 재발 방지 메모리 신설** | **완료 (실효)** | C17-3 보강분 작성 완료 | 2026-04-16 상위 규칙 C17 폐기(단일 세션 전환)로 실효 | - | | 12 | 2026-04-15 | (PD님 직접 지시) **C17 신설 — 세션 이동 지시 시 복사 가능 명령어 동봉 의무** | **완료 (실효)** | C17 신설 당시 완료 | 2026-04-16 단일 세션 전환으로 C17 자체 폐기되어 목적 소멸 | - | | 3 | 2026-04-14 | (총괄PM 경유) 시뮬레이터 이원화 해소 작업 착수 + 06번 설계안 문서 작성 | **완료** | `프로젝트/수상한잡화점/개발/06_신규코어_설계안_v1.md`, `07_시뮬레이터_이원화_해소_착수계획_v1.md` | - | 착수·문서 작성 완료. 후속 진행은 #28(시뮬레이션 환경 구축)에서 통합 관리. Unity MCP 활용 방향으로 전환(2026-04-17) | diff --git a/공유/PD_지시_트래킹/기획팀_PD_지시_로그.md b/공유/PD_지시_트래킹/기획팀_PD_지시_로그.md index bbaa591..5b10d76 100644 --- a/공유/PD_지시_트래킹/기획팀_PD_지시_로그.md +++ b/공유/PD_지시_트래킹/기획팀_PD_지시_로그.md @@ -46,6 +46,10 @@ C3·C13 위반에 해당. **즉시 자진 보고 후 소급 등록**. | # | 일시 | 지시 요지 | 처리 상태 | 산출물 경로 | 중단 사유 | 사후 조치 | |---|------|----------|----------|-----------|----------|----------| +| 35 | 2026-04-17 | 밸런싱 md 4종 변경 이력 테이블 표준화 (팀장 재량 진행 일괄 승인) | **완료** | `프로젝트/수상한잡화점/기획/스테이지난이도곡선_v1.md`·`밸런싱전략_v1.md`·`전체테이블감사_v1.md`·`빌드_조건_충돌점검_v1.md` 각 하단 "변경 이력 (P16 산출물 추적성)" 섹션 신설 + 초기 행 기입 | - | 차기 밸런스 변경 시 표준 포맷으로 1행 append. 필드: 일시/변경자/변경 필드/이전값→이후값/재미 근거/관련 PD 지시# | +| 34 | 2026-04-17 | 전문가 에이전트 6종 기록 의무 명시 + 구 P20 잔존 제거 (팀장 재량 진행 일괄 승인) | **완료** | `.claude/agents/balance-designer.md`·`content-designer.md`·`level-designer.md`·`narrative-designer.md`·`system-designer.md`·`ux-designer.md` 각 파일 "공통 업무 규칙" 섹션 교체 + "기록 의무 (영역 특화)" 섹션 신설. 구 P20(일일보고) 문구 전량 제거, SKILL.md 단일 SOT 참조로 통일 | - | PM이 `.live/` 더미 반영 예정. 차기 감사 시 plan-auditor로 준수 여부 교차 검증 | +| 33 | 2026-04-17 | 밸런스 요구서 표준 템플릿 신설 (팀장 재량 진행 일괄 승인) | **완료** | `공유/소통/기획팀→개발팀/REQ-템플릿_밸런스수치.md` (9개 섹션: 요구서 식별·변경 필드 목록·변경 전후 수치·재미 근거(C7)·개발 관점 우려 예상(C11)·검증 방법·백업 이력(C6·P16)·기각안(P24)·응답 섹션) | - | 향후 밸런스 수치 요청은 본 템플릿을 복사하여 사용. 템플릿 개선 필요 시 변경 이력 테이블에 반영 | +| 32 | 2026-04-17 | 어뷰징 판정 솔루션 기획 — 시뮬레이터 경계값 기반 클라/서버 검증 체계 설계 (기획팀 주도) | **완료** | `공유/소통/기획팀→PM/2026-04-17_어뷰징판정_솔루션_기획서_v1.md` (A~G 7개 섹션 + 기각안 5종). Unity MCP 시뮬 가동 후 경계값 확정은 후속 작업 | - | PM 검토 → 개발팀 F 섹션 인계 → Unity MCP 시뮬 가동(별도 PD 지시) → 경계값 테이블 v1.0.0 산출 → balance-designer 마진 재검토 | | 31 | 2026-04-17 | P24 "기각안" 필드 필수화 — 헌법 제1원칙 목표 2 원칙 B(인사이트 기록) 직결. 기획팀장 `2026-04-17_업무공유체계_점검_기획팀.md` 안건 1 채택 | **완료** | SKILL.md P24 본문 개정(결정·설계 엔트리 필수화 + 기각안 필드 필수화 근거 섹션 신설). 적용 주체: PM·팀장급·전문 에이전트 6종·3축 감사관 공통 | - | 기획팀장·개발팀장.md에 P24 기각안 필수 지침 명시. 향후 기각안 기록률 주기 점검 | | 30 | 2026-04-17 | 기획팀장 맥락 오류(plan-auditor "미신설" 오인) 원인 점검 + 재발 방지 조치 | **완료** | (1) 원인 2중 진단: 기획팀장.md·개발팀장.md가 폐기된 P20(일일보고) 잔존 + P24·P26·P27 미반영 + PM이 Agent 호출 시 최신 헌법급 변경 요지(d33b8be) 프롬프트 누락 (2) 조치: SKILL.md P27-2 "호출 프롬프트 필수 3요소" 추가, 기획팀장·개발팀장.md에 P24·P26·P27·3축 감사관 지침 신설, 구 P20 지침 제거 | - | PM 호출 프롬프트 체크리스트 운영 강제 — 차기 Agent 호출 시 (가)활성 PD 지시 요약 (나)최근 헌법급 변경 요지 (다)관련 신규 에이전트·도구 3요소 필수 포함 | | 27 | 2026-04-16 | 유니티 프로젝트 현재 상태 점검 — 기존 분석 산출물(개발/ 10건, 기획/ 12건) 유효성 교차 검증 | **완료** | `공유/소통/기획팀→PM/2026-04-16_유니티프로젝트_점검_기획팀.md` (8,683 bytes 실측 확인) | - | 후속: xlsm SOT 확정, Spine 도입 현황 개발팀 확인, GameManager.cs 소재 파악 (별도 신규 지시 필요 시 등록) | diff --git a/공유/대화로그/수상한잡화점/2026-04-17.md b/공유/대화로그/수상한잡화점/2026-04-17.md index 60dbfec..82218c7 100644 --- a/공유/대화로그/수상한잡화점/2026-04-17.md +++ b/공유/대화로그/수상한잡화점/2026-04-17.md @@ -1,5 +1,52 @@ # 수상한잡화점 — 2026-04-17 + +## [PM 기록 보완] 기획팀 재량 3건 수상한잡화점 영향 엔트리 (plan-auditor Major M1 시정) +- **요지**: 기획팀 #33 밸런스 요구서 표준 템플릿 신설 / #34 전문가 에이전트 6종 기록 의무 명시 + 구 P20 제거 / #35 밸런싱 md 4종(스테이지난이도곡선·밸런싱전략·전체테이블감사·빌드_조건_충돌점검) 변경 이력 테이블 표준화 — 3건 모두 완료 +- **이유**: 플랜 감사관이 "수상한잡화점 대화로그 누락(조직운영 로그에만 기록)" Major 지적. REQ 템플릿·밸런싱 md 4종은 수상한잡화점 프로젝트 영향이 명확하므로 프로젝트 로그 병기 필요. P27-4 SOT 경계 준수 +- **산출물**: `공유/소통/기획팀→개발팀/REQ-템플릿_밸런스수치.md` / `.claude/agents/{balance,content,level,narrative,system,ux}-designer.md` / `프로젝트/수상한잡화점/기획/{스테이지난이도곡선_v1,밸런싱전략_v1,전체테이블감사_v1,빌드_조건_충돌점검_v1}.md` 하단 "변경 이력(P16)" 섹션 +- **기각안**: (1) "조직운영 로그만 기록" — 프로젝트 영향 있는 결정은 프로젝트 로그도 병기 필요 (P27-4), 기각 (2) "REQ 템플릿을 개별 요구서처럼 PD 지시 로그 #화" — 상시 참조 템플릿은 진행 상태 없음, 소통 허브 유지 +- **상태**: 완료 + + +## [PM 기록 보완] 코어코드 레포 git 점검 기록 (dev-auditor Minor 시정) +- **요지**: Tier 1 잔여 9종(Attribute 3 + Util 6) 구현은 외부 레포 `코어코드/NerdNavis.Framework/`에 수행됨. C30 준수 — 작업 착수 시점에 코어코드 레포 git 상태 `nothing to commit, working tree clean` 실측 확인(개발팀장 세션 내부)되었으나 대화로그 1차 엔트리에 기록 누락 +- **이유**: dev-auditor Minor 지적 — C30-3 점검 결과 증적 미기록. 사후 추적 가능하도록 1줄 보완 +- **기각안**: 없음 (명백한 누락 보완) +- **상태**: 완료 + + +## [세션 시점] 서버 개발자 지시서 v1.1 요약판 재작성 완료 (#31, v1.0 대체) +- **요지**: PD님 2건 직접 재결정 반영. (1) 어뷰징 판정 책임 = **클라 100%**, 서버는 클라가 보낸 `is_abuse_flag` 수신 시 지급 거부만 수행 (경계값 보관·검증 전부 제거). (2) v1.0 446줄 → v1.1 207줄 요약판으로 전면 재작성 (인간 서버 개발자 5~7분 완독). +- **이유**: v1.0의 B-7 "어뷰징 방지 서버 연계" 섹션은 "서버가 경계값 받아 검증" 구조로 설계되어 있었으나 **어뷰징은 서버 개발자 작업 스펙이 아니라 클라 주도 작업**이라는 PD님 재확정으로 전면 정정 필요. 동시에 장문 지시서는 인간 개발자 파악 효율 저하 → 요약판 전환 지시. +- **주요 변경**: (a) 섹션 5 "어뷰징 방지 — 클라 주도, 서버 최소 역할"로 정정, Title Data 경계값 적재·IsWithinAbuseThreshold 함수 구조 전부 삭제. (b) 기본 원칙 4종 + 서버 스택 현황 표 + 8개 도메인 표 + 3원칙 + API 3종(샘플 1건만 JSON) + 셋업 체크리스트 + 결정 대기 2건 + 용어집 9개 + 기각안 5건. (c) frontmatter version v1.1, supersedes v1.0 명시. +- **산출물**: `공유/소통/개발팀→PM/2026-04-17_서버개발자_업무지시서_최종본.md` (v1.1, 207줄). DOCX는 PM이 `md_to_docx.js`로 재생성 예정. +- **상태**: 완료 +- **기각안**: (1) "어뷰징 경계값 서버 보관·검증" (v1.0 B-7 구조) — PD님 재결정으로 전면 폐기. 서버 개발자 작업 스펙 아님. (2) "상세 설계 전부 포함 장문 지시서" (v1.0 446줄) — 인간 개발자 파악 시간 낭비, 기각. (3) "v1.0 부분 수정" — B-7 전면 재설계 필요 규모, 요약판 원칙 병행 시 전면 재작성이 근본 해결. (4) "샘플 API 전량 제거" — `Save_StageResult` 샘플 1건은 `is_abuse_flag` 인터페이스 실체 예시로 유지 필요. (5) "PD-⑤ 리셋 시간 기준을 결정 대기에 유지" — 개발팀 재량 확정 사항으로 분리 표기. + + +## [PM] 인간 작업자 전달용 DOCX 2종 제작 완료 +- **요지**: PD님 지시("인간 작업자 전달용 문서는 PPT/WORD 등 전달 용이한 파일로 제작") 이행. 서버 개발자 지시서 + 어뷰징 판정 기획서 각 DOCX 변환 완료 +- **이유**: 인간 서버 개발자 배정 시 즉시 온보딩 가능한 공식 전달 자료 확보. md 원본은 조직 내부 자산으로 유지 +- **변환 방식**: pandoc 부재로 `scripts/md_to_docx.js`(docx-js 기반) 신설. 한국어 폰트 Malgun Gothic, A4, 표·목차·헤딩 위계·기각안 섹션 보존. 재사용 가능 +- **산출물**: + - `공유/인간작업자_전달자료/2026-04-17_서버개발자_업무지시서_v1.0.docx` (27KB) + - `공유/인간작업자_전달자료/2026-04-17_어뷰징판정_솔루션_기획서_v1.0.docx` (21KB) + - `scripts/md_to_docx.js` (재사용 변환 도구) +- **기각안**: + 1. PPT 형식 — 서버 개발자는 API·스키마 등 상세 기술 문서가 필요하므로 DOCX가 적합. PPT는 개요·요약 용도 + 2. pandoc 설치 — 환경 의존성 추가 부담. docx-js가 npm 단일 의존으로 더 가벼움 + 3. md 원본 그대로 전달 — PD님이 "전달 용이 파일" 명시, md는 개발자 도구 없으면 가독성 낮음 +- **상태**: 완료 + + +## [세션 시점] 인간 서버 개발자 업무 지시서 최종본 작성 완료 (#30) +- **요지**: PD 확정 3건(모든 보상=재화 통일 / 어뷰징 방지 솔루션 기획팀 주도 / DOCX 제작) 반영하여 #29 초안을 최종본으로 승격. B-7 어뷰징 방지 서버 연계 신규 섹션 + 1페이지 개요 + API 계약서 템플릿(Save_StageResult 샘플) + 용어집 + I 기각안 강화 포함. +- **이유**: PD님 2026-04-17 일괄 결정 3건으로 초안의 PD-①·② 안건이 소멸. 비재화 보상 관련 설계·안건 전면 제거 후 서버 검증 경계값 수용 구조를 기획팀 솔루션 수령 대비 설계. 인간 개발자 온보딩 단독 사용 가능 완결 문서로 재구성. +- **산출물**: `공유/소통/개발팀→PM/2026-04-17_서버개발자_업무지시서_최종본.md` (v1.0). 기존 초안 `공유/소통/완료/2026-04-17_RPT_서버역할_정리_초안.md`로 이동. +- **상태**: 완료 (md 최종본). DOCX 변환은 PM `anthropic-skills:docx` 수행 예정. +- **기각안**: (1) "초안 그대로 DOCX 변환" — PD 확정 미반영으로 인간 개발자가 낡은 안건 기반 설계 착수 위험, 기각. (2) "비재화 보상 유지(PD-① A/B안 존속)" — PD 직접 결정으로 전제 소멸, 기각. (3) "어뷰징 방지 서버 단독 설계" — PD 직접 결정(기획팀 주도)으로 기각, 서버는 수용 구조만 담당. + ## [12:21] Unity MCP 시뮬레이션 방향 전환 기술검토 완료 - **요지**: PD #28에 대한 Unity MCP 기반 시뮬레이션 기술검토 수행. 07 Headless 추출안 대비 Unity MCP `execute_code` + EditMode 경로가 결정론·유지비·기획팀 접근성 3축 모두 우위로 판단, 방향 전환 권장. @@ -15,3 +62,52 @@ - **산출물**: `공유/소통/기획팀→PM/2026-04-17_Unity_MCP_시뮬레이션_기획검토_기획팀.md` - **상태**: 완료 (검토 보고까지. 실제 Phase 3 재개는 PD님 재개 지시 후) - **기각안**: (a) 기획팀 MCP 도구 직접 호출 — 학습 부담 과대·역할 경계 교란 (b) Python 시뮬 아카이브·삭제 — C6 데이터 보호 위반 + Phase 3 v1 붕괴 원인 분석 근거 소실 리스크 + + +## [세션 시점] 인간 서버 개발자 업무 지시서 초안 작성 +- **요지**: PD #29 인간 서버 개발자 업무 지시 초안 작성. 서버 관리 데이터 카테고리 8종·클라/서버 경계 매트릭스 6개 액션군·PD 결정 안건 5건 정리. 현 수상한잡화점 `ServerClass.cs`·`ServerInfo.cs` 실측 — PlayFab CloudScript + 로컬 하이브리드 구조 확인. +- **이유**: PD 가이드 3종(재화 서버 필수·미션 보상 지급 주체 결정·랭킹 그대로 저장) 기본 적용. 인간 개발자 즉시 착수 가능 수준의 구체 필드·API 예시까지 포함. 메타 시스템 미파악분은 "추가 분석 필요"로 정직 명시. +- **산출물**: `공유/소통/개발팀→PM/2026-04-17_RPT_서버역할_정리_초안.md` +- **상태**: 진행중 (초안 + PD 결정 안건 제시. PD 결정 수령 후 설계 문서화 단계로 이행) +- **기각안**: (a) 서버 처리 최소화·전부 클라 주도 — PD 가이드 1 충돌 (b) 모든 비재화 자산 서버 SOT 전환 — PD 전제 초과·공수 과대 (c) PlayFab 전면 폐기 + 자체 서버 신규 — 서버 Critical 3건 보류 중 범위 확대 리스크 (d) 인간 개발자 배정 전 세부 API 스펙 확정 — 개발자 기술 선호 반영 없는 스펙은 재작업 리스크. **Agent 호출 기각**: `Task` 서브에이전트(서버팀장·클라이언트팀장) 호출이 본 세션 도구 환경 제약으로 불가, 개발팀장 단독 분석으로 대체하되 관점 명시 구분 (C23 정직성) + + +## [세션 시점] 어뷰징 판정 솔루션 기획서 v1 작성 완료 +- **요지**: PD #32 지시 대응. 시뮬레이터 이론 극값 × 안전마진계수 기반 경계값 도출 방법론 + 2계층(클라/서버) 검증 + F1/F2/F3 3단계 플래그 체계 + 경계값 테이블 JSON 스키마 + 서버 API 호출 흐름까지 설계 원칙·프레임워크 수준으로 정리. 실제 수치는 Unity MCP 시뮬 후 확정(v1은 틀만 제공). +- **이유**: 보상 재화 통일 결정에 따라 스테이지 클리어·랭킹·미션에서 클라 입력 검증 체계 필수. 서버 재시뮬 방식은 G-1 기각(자원 낭비·싱글 플레이 구조 오버엔지니어링), 경계값 비교 방식 채택. 재미 우선(C7) — false positive로 정상 플레이어 피해 최소화 위해 벡터별 5~20% 안전마진 설계. P17 ★조건 교차 검증 포함. +- **산출물**: `공유/소통/기획팀→PM/2026-04-17_어뷰징판정_솔루션_기획서_v1.md` +- **상태**: 완료 (설계·프레임워크 수준. Unity MCP 시뮬 가동·경계값 테이블 v1.0.0 산출은 후속 PD 지시 대기) +- **기각안**: (1) 서버 단독 재시뮬 검증 — CPU 비용·난수 동기화·싱글 플레이 오버엔지니어링 (2) ML 기반 이상치 탐지 — 초기 출시 학습 데이터 부재, 장기 보조 수단으로 여지 유지 (3) 클라 단독 판정 — 메모리 조작으로 무력화, 원천 제외 (4) 전체 플레이 로그 전송 — 네트워크·저장소 비용 과도, F3 확정 시 사후 감사용 부분 로그만 서버 재량 (5) 안전 마진 0% 엄격 검증 — false positive 폭발, C7 위반 + + +## [16:30] 서버 작업 참고 자료 v1.2 재작성 (외부 서버 작업자용 중립화) +- **요지**: v1.1(조직 내부용 서버 개발자 지시서)을 기반으로 외부 작업자용 중립 참고 자료 v1.2 신규 작성. PlayFab 전제 제거(현 사용 중 상태로만 기술)·조직 내부 프로세스 내용 전면 제거·문서 성격 재정의(지시서 → 참고 자료). +- **이유**: PD님 직접 지시 — 외부 서버 작업자에게 전달할 때 조직 내부 프로세스 용어(코어룰 참조·PD 지시 번호·결정 대기 안건)가 노출되면 부적절하며, 특정 스택(PlayFab) 강제 전제는 작업자 자율 판단 여지를 박탈. v1.1은 조직 내부용 상세본으로 보존하여 외부·내부 자료 분리. +- **산출물**: `공유/소통/개발팀→PM/2026-04-17_서버_작업_참고자료.md` (v1.2, 신규). v1.1 원본 유지. +- **상태**: 완료 +- **기각안**: (1) v1.1 직접 수정 (덮어쓰기) — 조직 내부 상세본 소실 위험, 외부·내부 분리 원칙 위반. 신규 파일 분리 채택. (2) 서버 스택 선택을 v1.2에서 확정 제시 — 외부 작업자 자율 판단 박탈, '열린 결정 사항'으로 중립 유지 채택. (3) 결정 대기 2건(PD-③·PD-④)을 각주로 축약 유지 — 조직 내부 미결 안건의 외부 노출 부적절, 전면 삭제 채택. (4) 기각안 섹션을 외부 자료에도 포함 — P24는 내부 규칙, 외부 자료 성격과 불일치. 대화로그(본 엔트리)에만 기록 채택. + + +## [세션 시점] Tier 1 잔여 9종 구현 완료 + 로그 경로 정규화 (PD 지시 #1·#5-A, PM 일괄 승인) +- **요지**: 개발팀장이 `코어코드/NerdNavis.Framework/` Tier 1 잔여 Attribute 3종 + Util 6종 구현 + 각 모듈 단위 테스트 추가 + CHANGELOG 갱신. PD 지시 로그 #1·#5 산출물 경로 정규화(구 경로·커밋 해시·glob 제거)로 `verify_log_paths.sh` 감사 15건 전수 실존 확인 통과. +- **이유**: PD님 2026-04-17 마무리 지시로 팀장 재량 진행 가능 작업 일괄 승인. 차단 요인 없음. OI-2 C+H1 승인 완료·Phase 3 재개 대기 등과 무관한 순수 구현 영역. +- **산출물**: `코어코드/NerdNavis.Framework/Runtime/Core/Attribute/` (ReadOnlyAttribute/ShowIfAttribute/ArrayTitleAttribute 3종) + `코어코드/NerdNavis.Framework/Runtime/Core/Util/` (EnumToInt/EnumEx/FormatEx/MathEx/KeyMaker/ValidationEx 6종) + `코어코드/NerdNavis.Framework/Tests/Runtime/Core/` (Attribute/Util 테스트 9종) + `CHANGELOG.md` 갱신 + `공유/PD_지시_트래킹/개발팀_PD_지시_로그.md` #1·#5 경로 정규화. +- **상태**: 완료 +- **기각안**: (1) 전체 Tier 1 16종 중 미구현 12종 일괄 구현 — Data/Event/Container 영역은 MonoSingleton·ServiceLocator와의 상호작용 설계 재검증 필요. 단일 응답에서 품질 보장 가능한 Attribute 3종 + Util 6종 = 9종 범위로 한정 채택. (2) 박싱 회피를 `Convert.ChangeType` 캐시로 우회 — 여전히 힙 할당 발생. `System.Runtime.CompilerServices.Unsafe.As<,>` 기반 근본 해결 채택(EnumToInt). (3) `KeyMaker` 구분자로 `'_'` 유지 — 수상한잡화점에서 `_`와 `:` 혼재로 조회 실패 경험. 프레임워크 표준 `:` 고정 채택. (4) 각 Util에 UnityEngine 참조 허용 — 서버/배치 컨텍스트 재사용 불가. 순수 BCL 의존만 채택(C11 범용성). + + +## [세션 시점] Phase 0-B 최종 완결 — UI 아키텍처(11) + 메타시스템(12) 문서 신설 +- **요지**: 수상한잡화점 파악 잔여 40% 중 UI·메타 영역을 `11_UI아키텍처_v1.md` + `12_메타시스템_v1.md`로 문서화. Assets 전수 `ls` + 키워드 `find` 실측 기반. 프레임워크 흡수 계획(Tier 1 UI 컴포넌트 + Tier 2 Save/Economy) 구체화. +- **이유**: PD님 2026-04-17 마무리 지시. 08(전투)·09(카드)·10(데이터) 완결 후 Phase 0-B를 UI·메타까지 확장. 헌법 제1원칙 목표 2(인사이트 기록 → 차기 프로젝트 참고)에 직접 기여 — 현 프로젝트 약점(*.Info 3역할·세이브 버전 관리 부재·재화 하드코딩) 차기 개선 안건 §10 기록. +- **산출물**: `프로젝트/수상한잡화점/개발/11_UI아키텍처_v1.md` · `프로젝트/수상한잡화점/개발/12_메타시스템_v1.md`. +- **상태**: 완료 +- **기각안**: (1) UGUI 19+19 스크립트 public API 메서드 전수 목록화 — 토큰 비용 과다, 구조·영향도 분류 목적에 불필요. 클러스터/LOC/범용성 3축 요약 채택. (2) UIToolkit 병행 매핑 문서화 — 기획 방향 UGUI 단일, 차기 프로젝트 R&D로 이관. (3) 메타시스템 보안 취약점 감사 — `05_서버연동_현황_v1.md` Critical 3건(#2 보류)과 중복. 본 문서는 구조·흡수 계획에 집중. (4) *.Info 클래스 필드별 세이브 대상 분류표 — 토큰 비용 + 프레임워크 흡수 대비 정보량 과다. 차기 Phase에 감사로 분리. + + +## [세션 시점] Phase 0-C Q-P1/Q-P2 응답서 + 시뮬레이터 전략 v2 (PD 지시 #5-B·#28) +- **요지**: 기획팀 Q-P1(4마리 노드 초반 위험도 의도성) + Q-P2(터치 방어 메커닉 3항)에 대한 개발 관점 응답서 발행. PD님 #28 Unity MCP 전환 반영하여 시뮬레이터 전략 v2로 재설정. +- **이유**: #5-B Phase 0-C 잔여 작업. 단, Q-P1은 기획 의도 영역이므로 **단독 답변 불가** 명시(C11 구분) + Q-P2는 초벌 스캔(`PCActor.cs` L37 실측)으로 50% 수치·쿨다운 정밀 수치는 2차 응답서로 분리. 시뮬레이터는 Python 폐기·Unity MCP 단일축 재확정 + 인프라 4종 설계 포인트 제시. +- **산출물**: `공유/소통/개발팀→PM/2026-04-17_Phase0-C_QP_응답서_개발팀.md`. +- **상태**: 완료 (Q-P2 2차 정밀 응답은 후속) +- **기각안**: (1) Q-P1에 "의도된 설계로 추정" 단정 응답 — 개발팀 의사결정 권한 외, C23 위반. 의도 선언은 기획팀으로 환송. (2) Q-P2 3항 전수 리버스 엔지니어링을 본 응답서에 포함 — 작업량 막대 + 범위 초과. 2차 응답서로 분리. (3) Python 시뮬레이터 복구 시도 — PD님 #28 "폐기 사안" 정면 위반. (4) Unity 외 3rd-party 시뮬레이터 검토 — PD님 Unity MCP 단일 방향 확정, 검토 대상 아님. + diff --git a/공유/대화로그/조직운영/2026-04-17.md b/공유/대화로그/조직운영/2026-04-17.md index d9eb9c1..6b77cde 100644 --- a/공유/대화로그/조직운영/2026-04-17.md +++ b/공유/대화로그/조직운영/2026-04-17.md @@ -236,3 +236,28 @@ - **산출물**: SKILL.md P24 엔트리 표준 형식 표 "기각안" 필드 "선택 (결정 시 권장)" → "**결정·설계 엔트리 필수**" 개정 + "기각안 필드 필수화" 근거 섹션 신설 (범위·근거·적용 주체·기입 방법·발의 출처) - **적용 주체**: PM·기획팀장·개발팀장·전문 에이전트(balance/content/level/narrative/system/ux) + 3축 감사관(pm/dev/plan-auditor) 공통 - **상태**: 완료 + + +## [기획팀장] 팀장 재량 진행 3건 일괄 완료 (PD 지시 #33·#34·#35) +- **요지**: PD님 "팀장 재량 진행 가능 작업 전건 일괄 승인, 마무리 지시" 수령. 3건 즉시 착수·완료 + 1. 밸런스 요구서 표준 템플릿 신설 → `공유/소통/기획팀→개발팀/REQ-템플릿_밸런스수치.md` (9개 섹션) + 2. 전문가 에이전트 6종 기록 의무 명시 + 구 P20 잔존 제거 → balance/content/level/narrative/system/ux-designer.md 6종 "기록 의무 (영역 특화)" 섹션 신설 + 3. 밸런싱 md 4종 변경 이력 테이블 표준화 → 스테이지난이도곡선·밸런싱전략·전체테이블감사·빌드_조건_충돌점검 v1.md 하단 P16 추적성 테이블 append +- **이유**: 조직 공유·기록 체계 일관성(P27) 강화 + 헌법 제1원칙 목표 2 원칙 B(인사이트 축적) 이행. 기획팀 산출물·개발팀 협업 요청이 단일 포맷·단일 경로로 집약되면 PM 교차 검증과 차기 프로젝트 참고 자료화 모두 용이 +- **기각안**: + 1. REQ 템플릿 "개발 관점 우려 예상" 섹션(C11 사전 제시) 제외 — 기획팀이 개발 영역 예측을 하는 것이 월권 소지 있으나, 명시적으로 "기획팀 예측·개발팀이 추가 발견 시 C3 제기" 구조로 명시하면 논의 효율 상승. 채택 + 2. 에이전트 6종 기록 의무를 각 파일에 중복 기재 대신 SKILL.md로만 일원화 — C14-4 참조 무결성 관점에서 매력적이나, 전문 에이전트가 자기 역할 맥락(영역 특화 원칙)과 함께 로드되는 것이 체감 준수율 상승. 영역 특화 부분은 각 파일 유지, 공통 의무는 참조형으로 축소 채택 + 3. 밸런싱 md 변경 이력 테이블을 `공유/대화로그/`로 일원화 — 추적성은 대화로그에 이미 있으나, 수치 문서 내 인라인 기록이 편집 시점에 즉시 눈에 띄어 누락 위험이 낮음. 현행 유지(문서 내 인라인) +- **산출물**: + - REQ 템플릿: `공유/소통/기획팀→개발팀/REQ-템플릿_밸런스수치.md` + - 에이전트 6종: `.claude/agents/{balance,content,level,narrative,system,ux}-designer.md` + - 밸런싱 md 4종: `프로젝트/수상한잡화점/기획/{스테이지난이도곡선,밸런싱전략,전체테이블감사,빌드_조건_충돌점검}_v1.md` +- **상태**: 완료 + + +## [PM] plan-auditor 모드 B 감사 — 재량작업 3건 일괄 완료 정합성 검증 +- **요지**: 기획팀장 #33·#34·#35 완료 후 plan-auditor 모드 B 호출. Critical 0·Major 1(수상한잡화점 대화로그 누락)·Minor 1·Improvement 2 도출 +- **이유**: P27-1 3축 감사 체계 신설 후 첫 plan-auditor 호출. 재량 작업 일괄 진행 시에도 프로젝트 축 대화로그 SOT 경계가 유지되는지 검증 +- **기각안**: 모드 A(응답 발신 직전) 호출 — 기획팀장이 이미 종료되어 사후 교차 검증이 필요, 모드 B 적합 +- **산출물**: `공유/소통/plan-auditor→PM/2026-04-17_감사보고_재량작업_일괄완료.md` +- **상태**: 완료 diff --git a/공유/서버_작업_참고자료/2026-04-17_서버_작업_참고자료_v1.2.docx b/공유/서버_작업_참고자료/2026-04-17_서버_작업_참고자료_v1.2.docx new file mode 100644 index 0000000000000000000000000000000000000000..ace010f33362204e0765258c8968883d0b1c6643 GIT binary patch literal 24558 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%0A+_^(s~ zj!zDgU#P^ej)ws&Gpl$qiBrxnLLh=^-?hihU6at*AbBm6Br4q={qVchdNwdDHF(*o zh5lmcr3cw5AdPzU*Z^sX8YU94DYY&O^4W0UBrf4MUIXn+1;|#Ds;B5F+E+~rT8z%N zxvs&~xC!>%gWUzNw|SoL>U~>}^g3M3``c6;jmMR{zy5udX6%Y*QD<1^nm2OTP>mp3 znOHV#@AH(wYdVzKmQQh40kg+?+x@|k#3Nt{PJ=vslxIoGb*N?8fPH>LY zV&0!%q7G=!k*annCjg4DP4aXUO>J_CUjHSw3ronYdU}pblLFQiq5Z*_ z@NHNDj)p<~7d7N-|4(|wGxKGgv}uRjiDMn{lG`Q-okp?#nJ+zPiBV|ub2NfKjaAgz zY_U479r1uDO+`gj6F(=CCJt`Sppz>ON7O3Hr`}8LLJ>cG)7?6EKkbI`0Ar9AmVzf` z#9b8#*Z;zJrP>pq4QiL$%S&gptyGvQOCxvZv8N(u9x~F!IN6g8uO6lIDMOPU}Gx$5i0`{!d`O-i8fWqUjdd1Upfw!k zvAfrAKbZe(B#^d+^Sr5@ZF%<6O-UpnK$JQay!mU|J*)TgNS?dhJ8?f6Zi)v^vR=8y ze>(l(eOIwN`^gz2@f=8NKG2(0&qBue!`yxLyRV@`*W!&v|L6Z{=PbOU+P()4$bf)! z$0$S#Ed*A=yH?!ufd(XL_ zea>35=ghu)@4Fu=_S23(x)Ojww{!c7?P=|}(fqvouFd6>^j9v_NI(>F^BO%u=#1iPQ2W|-)*VNUMV}9V(Cobx$rRK{>vNNEb zq#y$oV@D%Krw3GW&`nx*Fl?AInh;&RoOHxVg;)DnDRth*gK1epZG$w##zWfmWOw)49Qi7Z;O?ZSS=|wbwIIj3~6l z(&!yMS&&+-uVz_~RG4GkxT5*E_)eMVTq8c}rpcdKGXTHadkfD%$TFqw9>KEsGw#mumuvZGToELgbwBPap3O^iQ9A=O-0%cK&kG ztfm}YNRQ_F)X9_+#}FmL!p37qT%6PA-T1IF4rlE8NBWsJ?V;$&Q+uCwk|Vwy zqUTbWovK6tjT5aqt1X%HzC8bd+EldZa$xmEUtq#%= zFwN%DrZGf#Ddx9+jeS*yjWr9rxFu_I(Kfy-DpK1Eqlyf+t^uFSxuNr~Ph!Cfl z4~NGok@4`FAk`ZXPduFa>IC3zTnFqi6cA6Ei@@QBJ^SG`{i@?tlZy8_E*aEbmMIfN zW4NhmEM=H$)>NB}ZM$y$xFHV}a$INOOUvzS$PeXDBd>j2z%q#|@jGqWSzdM@Ci=3> zWg8~Z%SM2)Z-Tw4?(4{$oS-E$S$j2D{1+waZ3OBX*~4rz&Mpj_4He0vkLfh z$ZN92DY@e=-A2 z5uuS;tvL>ZR55}c91%QAX@u)sdJ*QOPBEk#*kdjP0;Uy|LBdxeWx^hFJ>p&W6(0i6 z&ZlQKAEO9h03WaeA^~wSzL~@{1xak`CtdmRYErX3=m+#AgVc}s+$6FSJXPfR$*iK6 z27L?y$i~4`M`={(gP_M+h3)PLa~Px+V;^6ZzwMmwG|x?<6gOmc@w!$~bSr5R(7&)2 z11$MPkr$1GBPVua@ay%u9oYePDjJ5dN9w0NQ5Q0IDmtjA+yXjSe&EGRGUhN*dSEaH zO$urwBi%accw=UX`pUj!dx`ExVFD zAYaW=0hclDz}ZP6SpwTMF0Q&7naoAEp0{%`}kwFB)kH;A3OBq?8Hpe z*Dd{F_MxrYn^THc89VD{UHmIAO^2YQOk)6_LAz6oeP1EwBf%FO9mS%pF=ODqEV`lo z^C9*khM}T~q5TYrLFbkd>eU_3hWFm7`gNt5aAH!%tge=Y zL{iWCOowvJr}RR)iaSU%8(Uz+oBO|yMTnr` zQ`}yftQ^#qqWl=Zr2eyu$$_hpkiKyk1%Cw!L+nCP>Hb{W&xvl=BlhnljwXcxLs1Vv zVqxdpVqtis1!AawukBCd_7sPnwFT$z)k~=PSEwbIj(tM@T9iA)a1PCDSB>>9dplZP z1U^tPuN<#Bzn~yvbKhTB_vB69k9ALQA_{L6#6pbOq~n zD}Ox7Jn!Tj{`Zi0utqujkkCAqInXAe9*s+zy66u?{O)9#VodRc>SD1`wH9$%q1ND5 zcpZ)be9MVxlcr)Fy_!_c;P*BtLlrT64o$_#6Y^H1PdN(gi6#)$1!IYPjV5SaLnmlE zl(#kI+zOfrD>4Gnxg46z9>AJ;BAZp47i{yb3lKh7qx!rYR9Vul3?;OLIq7aYOm91e zZqW~a@hb;4VD%zufZ{)NXDz{v62<|hv0 zD$xGDj#+9R6rdo24Nem0#p+X*L-dgkd>?-}^<6xZ2ZNBpsBN5U8F1-qBQ$L zAKmy(`qAVD)~waw;VA`bh+Ou&C{moDYZhYXdfTRXgHr4&`PCSNS)zF2*23XC2wNP> z80i(%qoN24{5Gz9>N6N!XdHM;o|Fwau4{IEtn56~5a)5VU&w&p9skCp>m0t1#~hF^ zp*uHI)heBO<3#_EbjX9BYz$iYnz4-T?Q+*1fFXC>6{K}0y&SpFu~K@nG`B7P;|MJ> z1u3Lq)88w0^4n8eaSi0Niz<9*<5EchS)-|I!feBg=)wcLqlTRCQB$KXv(uL5vMuM! zlONX)XGX;tZpwk`zM4b*qn6@DQ}Y1$L%-(d$IHz#mh4ufzNMYbFKKe3?3-(diJuoa zFYji$h3&t4Au5?@Av!A4WPoDLrYuQk8F=QEle}c*gRG*B*GidlkTK$y%2>J|c8cIF1@^sBo}|jnuew6n^+joQs#H$%^fgSJ z<^}XQ?c>S5i*BG63pl#w#|+@^anCO_TgXb!c>}$_9cDQWbXaxYe19b5_x<9i`-di7 zY3U1Z(H15XL<}2e>&C8&n~`eP3<3%y;X=dM{%9S100?Djx>=uYRH9=|sOZaj5ic?V68LKrAg@%J0= z+^o`D;ViPK(6tjsDi>9(|0tq-^KGrH`@1C*t>c<#CsYC`aS3ww22S}+i+*jddE}?s z#p&Lcoiv(qllBZ{?0PuBA4<*`47a(06J!gg_5C75_c@`V8OwVs zAHz5S?v5nQWghJ(Gs$M$6VH&qMG2+MBTG9_GX?gY3~c#=AV_v^;@bJ8^&CF%lW@<~ z=d!V26a50OXCz0^VAWyL>2ZZR0Irax71yb;t!+idQ!*%u^cp+Mh3d0_(o;dh#Gtx( zT5SzTKf_58()N|FjVJ&+*4AvlM)x9Ou>ikX5@Ry#)XMFcGwH+7IDtn(>S|&Cs8YW% zILe#8cqFMXwP58CZ}S%Zc3oP)s+Ou7o7kD(x-PSm zO!&ifYM1Ucj42amX)cmI$ zyWZ#L6ASK{bk+)R+a(#Onfa?L&Lm*)2YIYHHR>eg@nnL5z9NIL;N5UHFVPg*$oFyv zP92!0G1BEjyDqf!W4z`qQ8j?1#du6EhQRXT5ie_)d!B4X^JC+D6uaq0j-<*l;^s+U zB$70F?+Z;S^JZigwU@7pj66gAsG=-;YDvk35MpSdZl*D|QuisLWV!C!F&JdI&Ac}) z=>+7@J;VmSTL5c7{d$Z^E1XAQ+jCl1(@1JQw#&SMs$D6v#)t)Y20TA24jmrr{(DX= zs}E86{aPThN#^iWyeaTxRXrhV!1G})Av1wI58XX-27S0ii?cLRyE4dqxToL0UhhN8 zHjZ~L%2*8j+Ne~C2}I-3;ZexLnZ$o^_7(VkPop z6{^m1r=_)zGdnz9$bw(y*K*5c=Rf0`LD0yc6+M1;e&XEK`htV4YSd6g-}0LRl4)8& z?mbM?r5BSD)k3V6+L63C&Qk!6UY}-kAzIOQDAa5ZgCLI|_OO6=&8ZjU0~lst(_C#s;zS1j5kTCUg^=v^P@!PsD=GdO| z1e-X~myb^w`y8*jCFKopRgf*tN&m}(&M#Yg)&$!JHejlV4a>lSrCljIJ36_rm^wQD zp@p#eod2J81=}EFAfmSOB6!0`!EcSzecT~ccGPBN`Ljuqz4#?9#wYFq>GFN!Zw}Tq zW0mK{dkGjMRIpxoY7d0?1LG$?c{o81OPS9%Kwodx<9UucJ+D~hX5eX=ocRVsr-tuS zP2oO%B7W`ExV<(YLrR02`T+;*iA3T!x4&d!AR(ybWcVIHODRHW3E|%w#5y8XlL)oy z!X%!1DlJ18=aRyRq5FUGi zR8l@+g4|-@=$90L{Rv?P3J1f6tSiv{O926pH%HND&Of^S$GsEJi^Guf%G`vBDqbU! zCxYyK#Ucmcc{|ZK%k>rAX}i8E3(=$gf>P&eW8a{dX5Etq`=ceh7uSf(MI2XK%O~^L zd1*&VpUSq=T}y{X@L7`>bkn5IpcVf9nm6vMPW2u3ulDa~(aG!0OJA6-Pr`H^>vvr@c5=Gi zaQ|P`VR{aGWvh-U!ZHkBQ66Ke9M02~01B!Z)I)hcLr7K{%pGyihMVowngXW4BKNnX z6gwK$kH-gU`5S4VrV%U4=0Z7mUSLiB68p|^!?BKrO2O!7+SD-(t>f`Tr})B8gVHtx z!NqJA62{x?j;be1F_Ozh3|pZ?sWd5Q)iU$o%r;p4V#gd`M_B0H01piu9;#Wue|pb& zox|X0F5p>8QchPZQ+)1M-sCsllsG444s-I!a!j@GVYAf6vTGE1xPqLYDQseV;bD)J z>;xD3`@;^5*CNQx3K1_qIcvg^!aHqu&ZttMrU}~+>$_@<<_hCm0^w_qvB?QZlVP<3 zDW6n;&@fjZ!OM8shKbGOd*r^jml{u;c8w^*dbt2S48{})SMWv$x-5*xbp;e%EnoNK zJfP;sO(&#e%c~#f?Or8NZJM~Z5s^NOH4>+#cV@LBf@>}7#*z1cKUg7IxX9=ZVeYfP z5xjI|quae2|3pf;-QTeZ3t;}#Sa<{`m__#Ah0Xu?dVXvEqs+Oo+&=~Uv-I+BEL+7!Iz5?I->}5`FJVx|`kkOV$sL|B>r?SH#^U zmR}-t1^x "카드 0장 상태에서 4마리 노드가 '터치 방어 없이는 위험'한 현재 상태는 의도된 설계? (=터치 방어를 자연스럽게 가르치기 위함?)" + +### 1-2. 개발팀 응답 (실증 기반) +**개발팀 단독 답변 불가 범주**. 이유: +1. "의도된 설계 여부"는 **기획 의도의 영역**이며 개발팀은 결정권이 없음 (C7·C11 구분) +2. 개발팀이 제공할 수 있는 것은 **코드 실측**(터치 방어 유무에 따른 생존 시나리오) + **구조적 힌트**(초기 4마리 노드가 카드 풀 구성 규칙과 맞물려 있는지) + +### 1-3. 개발팀이 제공 가능한 근거 +- `Assets/Script/InGame/Actor/PCActor.cs` L37: `eActorStatus.Defecne` + `InGameInfo.Ins.Set_PCBlock` → 터치 방어는 PC(플레이어 캐릭터) 상태 효과로 실체화되어 있음 +- 카드 0장 상태의 실측 DPS·EHP는 개발팀 Unity MCP 시뮬레이션으로 제공 가능 (§3 전략 참조) + +### 1-4. 권장 진행 방향 +- 기획팀이 **의도 명확화 선언** → 개발팀이 Unity MCP로 해당 설계가 수치적으로 성립하는지 검증 +- 구체적으로: 기획팀이 "터치 방어 없이 4마리 처리 가능 여부"의 목표 비율(예: 80% 생존)을 수치화 → 개발팀이 시뮬레이션으로 현 수치가 목표를 만족하는지 응답 + +## 2. Q-P2 응답 — "터치 방어 메커닉 정확도" + +### 2-1. 기획팀 질의 요지 +1. 터치 1회 = 다음 공격 1회 피해 50% 감소? +2. 터치 유지 중 계속 50% 감소? +3. 쿨다운? + +### 2-2. 개발팀 응답 (코드 실증 기반, 미확인 항목 명시) + +**확인된 사실** (실제 `PCActor.cs`·`Actor.cs` 실측): +- 터치 방어는 `eActorStatus.Defecne`(원문 오타 `Defecne`) 상태 효과로 모델링 +- `InGameInfo.Ins.Set_PCBlock` 을 통해 블록 스프라이트·상태를 설정 +- "블록(Block)" 용어와 "방어(Defense)" 용어가 **혼용**되어 있음 (현 구조의 약점) + +**미확인 — 정밀 코드 리뷰 필요**: +- 50% 감소 수치의 실제 테이블 위치 (Actor·Effect·Card 중 어디에 기록?) +- 감소 비율이 **고정값인지 카드·장비·인장 옵션에 따라 변동**하는지 +- 쿨다운 존재 여부·값 +- "1회성"인지 "지속형"인지 (상태 효과 duration 필드가 별도 관리되는지) + +**개발팀 차기 조치** (본 응답서 발신 후 즉시 착수 가능): +1. `Actor.cs`·`PCActor.cs`·관련 `Effect*.cs` 정밀 리뷰하여 수치 위치 식별 +2. 기획팀 `StatusEffect` 마스터 테이블 확인 +3. Q-P2 3문항 각각에 **코드·테이블 실측 근거 포함 2차 응답서** 제출 + +### 2-3. 본 응답의 한계 +본 응답은 **초벌 스캔 결과**이며, 50% 감소·쿨다운 확정 수치는 미확정. Q-P2는 **2차 응답서로 완결** 필요 (C23 정직 보고). + +## 3. 시뮬레이터 전략 업데이트 (07 문서 후속) + +### 3-1. 배경 변경 +- **PD님 2026-04-17 직접 지시 (#28)**: 기획팀 시뮬레이션 작업은 **Unity MCP 기반**으로 전환 +- Python 시뮬 파일은 **소실 확정 — 폐기 사안** +- 교차 검증 축 이원화 방침 폐기 → **Unity MCP 단일 축**으로 확정 + +### 3-2. 재설정된 시뮬레이션 전략 + +| 항목 | 결정 | +|------|------| +| 시뮬레이션 실행 환경 | Unity Editor + Unity MCP (`mcp__unity-mcp__*`) | +| 결과 취득 방법 | `execute_code`·`run_tests`·`manage_editor` 등으로 플레이 모드 제어 + 로그/결과 수집 | +| 시뮬레이션 코드 위치 | `Assets/Script/Server/`·`Assets/Script/InGame/` 등 기존 프로젝트 내부 (별도 시뮬레이터 레포 불필요) | +| 성능 모드 | Headless 불필요 — 기획팀 밸런싱은 시각적 피드백 포함이 유리. 배치 검증 시에만 PlayMode 자동화 | +| 결과 저장 | `기획팀/.cache/` 또는 `공유/시뮬결과/` (기획팀 결정) — 개발팀은 실행 환경 제공까지 | + +### 3-3. 개발팀이 제공할 인프라 (Phase 0-C 잔여) +1. **시뮬레이션 진입점 스크립트**: 전투만 격리 실행 가능한 `SimulationRunner.cs` 프로토타입 (PlayMode 의존 최소화) +2. **파라미터 외부화**: 앵커 전투 시나리오(몬스터 구성·PC 상태·카드 0장 등)를 JSON/ScriptableObject 외부 입력으로 받도록 리팩토링 포인트 식별 +3. **결과 수집 포맷**: 시뮬레이션 1회 결과를 `{timestamp, scenario_id, turns, dmg_taken, survived, ...}` 구조화 JSON 출력 +4. **Unity MCP 호출 템플릿**: 기획팀장이 `Task` 또는 `/게임플레이` 에이전트 호출 시 바로 쓸 수 있는 MCP 호출 스니펫 (3종: 단일 실행 / 파라미터 스윕 / 배치 비교) + +**작성 예정 산출물** (본 응답서 뒤 착수): +- `공유/소통/개발팀→PM/2026-04-17_Unity_MCP_시뮬레이션_인프라_설계_v2.md` — 상기 4종 인프라의 구체 설계 +- 기존 `공유/소통/개발팀→PM/2026-04-17_Unity_MCP_시뮬레이션_기술검토_개발팀.md` (이미 존재, #28 참조) 위에 **실행 인프라 단계** 추가 + +### 3-4. 기획팀 작업 재개 조건 +- 위 3-3의 인프라 4종 중 최소 (1)·(3)·(4)가 완료되면 기획팀이 앵커 전투 시뮬레이션 작업 재개 가능 +- (2) 파라미터 외부화는 병행 진행 가능 (선택사항) + +## 4. 기각안 (P24) + +- **기각안 A: 전투 메커닉 전수 자동 리버스 엔지니어링 + 본 응답서에 통합 수록** — 작업량 막대 + 본 응답서 범위 초과. Q-P2 2차 응답서로 분리 +- **기각안 B: Python 시뮬레이터 복구 시도** — PD님 "폐기 사안" 확정(#28 비고란). 복구 시도 자체가 지시 위반 +- **기각안 C: Unity 외 3rd-party 시뮬레이터 도입 검토** — PD님 Unity MCP 단일 방향 확정. 검토 대상 아님 +- **기각안 D: Q-P1에 "의도된 설계로 추정됨" 단정 응답** — 개발팀 의사결정 권한 외. C23 위반 회피 + +## 5. 후속 작업 · 블로커 + +### 5-1. 개발팀 즉시 착수 항목 (본 응답서 발신 후) +1. Q-P2 정밀 2차 응답 (코드·테이블 실측) +2. §3-3 인프라 4종 (1)·(3)·(4) 구현 +3. Unity MCP 호출 스니펫 문서화 + +### 5-2. 기획팀 필요 액션 +1. Q-P1에 대한 **기획 의도 선언** (의도된 설계? 개선 필요?) +2. §3-3 시뮬레이션 결과 저장 위치 결정 +3. Q-P2 정밀 응답 수령 후 터치 방어 정책 재확인 + +### 5-3. PM 조율 필요 +- 없음 (팀 자율 진행 가능 범위, C29-1 정합) + +## 부록. 변경 이력 +- **v1 (2026-04-17)**: 초판. 개발팀장 Phase 0-C Q-P 응답 + Unity MCP 전환 반영 시뮬레이터 전략 v2 초안 작성. diff --git a/공유/소통/개발팀→PM/2026-04-17_서버_작업_참고자료.md b/공유/소통/개발팀→PM/2026-04-17_서버_작업_참고자료.md new file mode 100644 index 0000000..3679689 --- /dev/null +++ b/공유/소통/개발팀→PM/2026-04-17_서버_작업_참고자료.md @@ -0,0 +1,186 @@ +--- +from: 개발팀장 +to: PM (DOCX 변환 → 외부 서버 작업자) +date: 2026-04-17 +type: 참고자료 +status: 완료 +version: v1.2 +audience: 외부 서버 작업자 +tags: [서버참고자료, 수상한잡화점, 서버경계, 데이터SOT] +--- + +# 서버 작업 참고 자료 — 수상한잡화점 + +> **본 문서의 성격**: 수상한잡화점 프로젝트 서버 파트 작업자에게 전달하는 **참고 자료**입니다. 공식 업무 지시서가 아니라, 현 프로젝트의 서버 현황·데이터 카테고리·클라-서버 경계 원칙·참고용 API 샘플을 정리한 배경 문서입니다. 세부 구현 방향과 서버 스택 선택은 작업자가 판단·제안할 수 있는 열린 영역으로 남겨져 있습니다. +> **권장 완독 시간**: 5~7분 + +--- + +## 0. 1페이지 개요 + +- **프로젝트**: 수상한잡화점 (로그라이크 RPG, Unity 2022+, 모바일) +- **현 서버 스택**: PlayFab 사용 중 (Title API·CloudScript·Leaderboard·Profiles·Mail). 유지·전환·하이브리드 여부는 열린 결정 사항 +- **서버 담당 영역 (현 프로젝트 범위)**: ① 재화 SOT 서버 검증·지급 ② 랭킹 저장·조회 ③ 우편·IAP 영수증 검증 ④ 서버 시간 기준 리셋 +- **어뷰징 판정 책임**: 클라이언트 주도. 서버는 판정 결과(플래그) 수신 시 지급 거부 처리만 수행 (섹션 5 참조) +- **현 상태**: 서버 Critical 보안 3건 보류 중, 서버 작업자 합류 시점 재개 예정 + +**프로젝트 기본 원칙 4종 (확정, 변경 불가)** +1. 모든 보상은 **재화 형태로 지급**. 비재화 보상(칭호·스킨·장비·PC 등) 없음 → 모든 보상은 서버 검증·지급 대상 +2. 재화 사용·획득은 **항상 서버 응답 필수**. 클라 단독 처리 금지 +3. 미션 클리어 판단은 **클라 재량**. 서버는 보상 지급 시점에만 개입 +4. 랭킹 등록은 **클라 정보를 서버가 그대로 저장** + +--- + +## 1. 현 서버 스택 현황 (PlayFab 사용 중) + +| 구성 요소 | 현 상태 | 비고 | +|----------|--------|------| +| PlayFab 인증·UserData·TitleData | 사용 중 | - | +| CloudScript | 사용 중 (`Get_UserInfo`·`Get_MailList`·`Get_MailReward` 등) | 함수 dump 후 버전 관리 편입 필요 | +| Leaderboard (Progression API) | 읽기 경로 존재 (`GetLeaderboard`·`GetLeaderboardAroundEntity`) | 등록 경로 미확인 → 재설계 필요 | +| Mail | `Get_AllMail` CloudScript 존재 | 재화 보상 우편 처리 | +| `Save_StageResult` (스테이지 완료 처리) | 비활성 상태 (주석 처리) | 복원·신규 설계 필요 | +| `InappInfo.cs` (IAP 영수증) | 비활성 상태 | 재활성화 + `ValidateGooglePlayPurchase` 연결 | +| `Server_Live_ID`·`Server_Dev_ID` | 코드 주석 처리 상태 | 인수인계 시 복구 | + +> **스택 선택은 열린 사안**: 본 섹션은 "현재 이렇게 구현되어 있다"는 현황 기술입니다. PlayFab 유지, 하이브리드 구성, 별도 스택으로의 전환 등 서버 아키텍처 방향은 작업자 판단·제안 가능 영역입니다. + +--- + +## 2. 서버 관리 데이터 카테고리 (8개 도메인) + +| # | 도메인 | 주요 필드 | SOT 방향 | +|---|--------|----------|---------| +| 1 | 재화 인벤토리 | `dic_Item[itemid] = amount` (Gold·Soul 등) | 서버 SOT 필수 | +| 2 | PC(캐릭터) 보유 | `dic_PC[pcid]` — 레벨·경험치·각성 | 서버 SOT | +| 3 | 장비·카드 | `Equipment`·`dic_Card[cardid]` | 서버 SOT | +| 4 | 스테이지 진행 | `StageData.dic_stagedata[diff][chapter][stageid].Star` | 서버 SOT (보상 지급 연결) | +| 5 | 미션·출석 | `Mission.dic_Mission[type]`·`RewardAttandanceDay` | 서버 SOT (재화 보상) | +| 6 | 랭킹 | Leaderboard (현 PlayFab) | 서버 저장 | +| 7 | 우편 | CloudScript 기반 (현 PlayFab) | 서버 SOT | +| 8 | 시즌·패스 | 신규 정의 필요 | 기획 SOT 정의 후 구체화 | + +--- + +## 3. 클라/서버 경계 — 핵심 원칙 3 + +1. **재화 사용·획득 → 서버 응답 필수** + - 클라: 사용·획득 요청 전송 / 서버: 잔고 검증·차감·응답 (또는 지급·응답) + - 서버 거부 시 클라 롤백 + +2. **미션 클리어 → 클라 판단, 재화 보상은 서버 지급** + - 클라: 조건 체크·카운트 누적·달성 판정 / 서버: 보상 수령 요청 수신 시 지급 + +3. **랭킹 등록 → 클라가 계산한 값을 서버가 그대로 저장** + - 클라: 점수·메타정보 계산·전송 / 서버: Leaderboard 저장 + +**보상 통일**: 모든 보상은 재화 단위. 비재화 보상(칭호·스킨·장비·PC 등) 없음. + +--- + +## 4. 핵심 API 참고 예시 + +> 아래는 **현 구현 참고용** 샘플입니다. 서버 스택·구현 방식이 변경될 경우 인터페이스는 재설계 대상입니다. "반드시 이 방식을 유지해야 한다"는 의미가 아닙니다. + +### 4-1. `Save_StageResult` (복원 필요 — 현 PlayFab CloudScript 기준 샘플) + +**유형**: CloudScript 함수 / **호출 권한**: Client (LoginSession 유효) +**역할**: 스테이지 클리어 결과를 저장하고 재화를 지급 + +**요청 파라미터** (예시): +```json +{ + "difficulty": 2, + "chapter": 3, + "stageId": 47, + "starCount": 3, + "clearTimeMs": 125000, + "droppedItems": [{"itemId": 101, "amount": 500}], + "is_abuse_flag": false +} +``` + +**응답 (성공)**: +```json +{ + "status": "ok", + "grantedRewards": [{"itemId": 101, "amount": 500}], + "newStar": 3, + "stageProgress": {"maxStageId": 47} +} +``` + +**응답 (실패)**: +| 에러 코드 | 조건 | 클라 처리 | +|----------|------|----------| +| `AbuseFlagged` | `is_abuse_flag: true` 수신 | 지급 거부, 기록 저장 (섹션 5) | +| `SessionInvalid` | 세션 없음·만료 | 재로그인 유도 | +| `StageLocked` | 해당 스테이지 미해금 | 경로 이탈 처리 | + +### 4-2. `Grant_MissionReward` +- 미션 보상 재화 지급. 클라가 미션 완료 판정 후 수령 요청 시 서버가 지급·응답 +- `is_abuse_flag` 동일 수용 (클라가 자체 판정 시 true 동봉 가능) + +### 4-3. 랭킹 등록 경로 (신규 설계 필요) +- 현 코드에서 등록 호출처 미확인 → 재구축 필요. 엔드포인트명 예: `Submit_RankingEntry` +- 클라 전송 점수·메타정보 그대로 Leaderboard 저장. 세부는 설계 단계에서 확정 + +--- + +## 5. 참고: 어뷰징 방지 체계 (클라 주도, 서버 최소 역할) + +> **프로젝트 정책**: 어뷰징 **판정은 클라이언트 책임**. 서버는 판정 결과(플래그)만 받아 처리합니다. + +**판정 책임**: 클라이언트 (경계값 보관·수치 검증 전부 클라에서 수행) + +**서버 역할 (최소)**: +- 재화 지급 요청 페이로드에 **`is_abuse_flag: true`** 가 포함된 경우, 해당 지급을 **거부**하고 **기록** 저장 +- 플래그 저장 위치: 현 스택 기준 Profile 필드 또는 별도 모니터링 엔드포인트 (구현 단계에서 결정) +- 관리자 알림 채널(Slack Webhook·이메일 등) 연계는 구현 단계에서 결정 + +**서버가 하지 않는 것 (프로젝트 정책)**: +- 경계값 테이블 보관 (Title Data 등에 적재 불필요) +- 경계값과 클라 전송 수치 비교 검증 +- 어뷰징 판정 로직 자체 + +**협의 범위**: 클라이언트팀 구현 시 필요하면 서버 작업자와 **플래그 인터페이스 스펙**만 협의합니다 (판정 로직 자체는 협의 대상 아님). + +--- + +## 6. 환경 셋업 체크리스트 + +- [ ] 현 서버 스택(PlayFab) 접근 권한 수령 (Title ID 인수·Game Manager 대시보드·CloudScript revision 접근) — 현 스택 유지 시 +- [ ] 현 배포 CloudScript 전문 dump → 버전 관리 편입 (경로 후보: `코어코드/수상한잡화점_서버/`) +- [ ] Unity 프로젝트 clone + `git fetch origin && git pull` +- [ ] Unity 에디터 설치 (프로젝트 버전 기준) +- [ ] 현 PlayFab SDK 연동 확인 (빌드·실행 1회 성공) +- [ ] IDE (VS Code 또는 JetBrains Rider) + Node.js (CloudScript 로컬 테스트용) + +--- + +## 7. 용어집 + +| 용어 | 뜻 | +|------|---| +| PlayFab | Microsoft BaaS. 본 프로젝트가 현재 사용 중인 서버 스택 | +| CloudScript | PlayFab 서버 사이드 JavaScript 실행 환경 | +| Title Data | PlayFab Title 단위 공용 설정 저장소 | +| UserData | PlayFab 유저별 서버 저장소 | +| Leaderboard | PlayFab 순위표 서비스 (Progression API) | +| ObscuredType | `CodeStage.AntiCheat` 로컬 변조 방어 타입. 서버 검증 대체재는 아님 | +| ServerData | 수상한잡화점 프로젝트 클래스(`ServerClass.cs`). 유저 데이터 SOT 로컬 표현 | +| SOT | Source of Truth. 데이터의 단일 진실 근원 | +| AntiCheat | 어뷰징·치트 방지 체계. 본 프로젝트는 클라 판정 + 서버는 플래그 수신 시 지급 거부 | + +--- + +## 8. 변경 이력 + +| 일시 | 버전 | 요지 | +|------|------|------| +| 2026-04-17 | v1.2 | 외부 서버 작업자용 참고 자료로 중립화 — PlayFab 전제 제거(현 사용 중 상태로만 기술), 조직 내부 프로세스 표현 제거, "지시서" → "참고 자료"로 성격 재정의 | + +--- + +**용도**: 서버 작업자 합류 시 본 자료를 출발점으로 사용. 세부 서버 스택 선택, 아키텍처 방향, API 스펙 확정은 배정 후 설계 단계에서 진행. diff --git a/공유/소통/개발팀→PM/2026-04-17_서버개발자_업무지시서_최종본.md b/공유/소통/개발팀→PM/2026-04-17_서버개발자_업무지시서_최종본.md new file mode 100644 index 0000000..0ba9d98 --- /dev/null +++ b/공유/소통/개발팀→PM/2026-04-17_서버개발자_업무지시서_최종본.md @@ -0,0 +1,207 @@ +--- +from: 개발팀장 +to: PM (DOCX 변환 → 인간 서버 개발자) +date: 2026-04-17 +type: 지시서 +status: 완료 +version: v1.1 +tags: [서버개발자지시서, 요약판, 서버역할, 클라서버경계, PlayFab, 수상한잡화점] +related: [C8, C11, C28, C29, C30, P13, P14, P18] +depends_on: [PD-#2, PD-#30, PD-#신규_어뷰징책임_재확정] +supersedes: v1.0 (2026-04-17 동일 경로, PD님 어뷰징 책임 재확정·요약판 재작성 지시로 대체) +--- + +# 인간 서버 개발자 업무 지시서 (요약판 v1.1) + +> **대상**: 인간 서버 개발자 (수상한잡화점 서버 파트 합류 예정) +> **읽는 시간**: 5~7분 완독 가능 수준으로 축약 +> **v1.1 변경 요지**: (1) 어뷰징 판정 **클라 100% 책임**으로 확정 (PD님 재결정). 서버는 클라가 보내준 **판정 플래그만 수신**하여 지급 거부. (2) v1.0 446줄을 요약판으로 전면 재작성. 세부 원문은 초안 `공유/소통/완료/2026-04-17_RPT_서버역할_정리_초안.md` 참조. + +--- + +## 0. 1페이지 개요 + +- **프로젝트**: 수상한잡화점 (로그라이크 RPG, Unity 2022+, 모바일) +- **서버 스택**: PlayFab 기반 (Title API·CloudScript·Leaderboard·Profiles·Mail) — 본 지시서는 PlayFab 유지 전제 +- **역할 범위**: ① 재화 SOT 서버 검증·지급 ② 랭킹 저장·조회 ③ 우편·IAP 영수증 검증 ④ 서버 시간 기준 리셋 +- **어뷰징 판정**: **클라 100% 책임**. 서버는 판정 결과(플래그) 수신 시 지급 거부만 수행 (섹션 5 참조) +- **현 상태**: 서버 Critical 3건 보류 중(PD 지시 로그 #2), 인간 개발자 배정 후 재개 + +**기본 원칙 4종 (PD 확정, 변경 불가)** +1. 모든 보상은 **재화 형태로 지급**. 비재화 보상 없음 → 모든 보상은 서버 검증·지급 대상 +2. 재화 사용·획득은 **항상 서버 응답 필수**. 클라 단독 처리 금지 +3. 미션 클리어 판단은 **클라 재량**. 서버는 보상 지급 시점에만 개입 +4. 랭킹 등록은 **클라 정보를 서버가 그대로 저장** + +--- + +## 1. 서버 스택 현황 + +| 구성 요소 | 현 상태 | 비고 | +|----------|--------|------| +| PlayFab 인증·UserData·TitleData | 사용 중 | 유지 | +| CloudScript | 사용 중 (`Get_UserInfo`·`Get_MailList`·`Get_MailReward` 등) | 함수 dump 후 git 편입 필요 | +| Leaderboard (Progression API) | 읽기 경로 존재 (`GetLeaderboard`·`GetLeaderboardAroundEntity`) | 등록 경로 미확인 → 재설계 필요 | +| Mail | `Get_AllMail` CloudScript 존재 | 재화 보상 우편 처리 | +| `Save_StageResult` (스테이지 완료 처리) | **비활성 상태** (주석 처리) | 복원·신규 설계 필요 | +| `InappInfo.cs` (IAP 영수증) | **비활성 상태** | 재활성화 + `ValidateGooglePlayPurchase` 연결 | +| `Server_Live_ID`·`Server_Dev_ID` | 코드 주석 처리 상태 | 인수인계 시 복구 | + +--- + +## 2. 서버 관리 데이터 카테고리 + +| # | 도메인 | 주요 필드 | SOT 방향 | +|---|--------|----------|---------| +| 1 | 재화 인벤토리 | `dic_Item[itemid] = amount` (Gold·Soul 등) | 서버 SOT 필수 | +| 2 | PC(캐릭터) 보유 | `dic_PC[pcid]` — 레벨·경험치·각성 | 서버 SOT | +| 3 | 장비·카드 | `Equipment`·`dic_Card[cardid]` | 서버 SOT | +| 4 | 스테이지 진행 | `StageData.dic_stagedata[diff][chapter][stageid].Star` | 서버 SOT (보상 지급 연결) | +| 5 | 미션·출석 | `Mission.dic_Mission[type]`·`RewardAttandanceDay` | 서버 SOT (재화 보상) | +| 6 | 랭킹 | PlayFab Leaderboard | 서버 저장 | +| 7 | 우편 | CloudScript 기반 | 서버 SOT | +| 8 | 시즌·패스 | 신규 정의 필요 | 기획팀 SOT 정의 후 구체화 | + +--- + +## 3. 클라/서버 경계 — 핵심 원칙 3 + +1. **재화 사용·획득 → 서버 응답 필수** + - 클라: 사용·획득 요청 전송 / 서버: 잔고 검증·차감·응답 (또는 지급·응답) + - 서버 거부 시 클라 롤백 + +2. **미션 클리어 → 클라 판단, 재화 보상은 서버 지급** + - 클라: 조건 체크·카운트 누적·달성 판정 / 서버: 보상 수령 요청 수신 시 지급 + +3. **랭킹 등록 → 클라가 계산한 값을 서버가 그대로 저장** + - 클라: 점수·메타정보 계산·전송 / 서버: Leaderboard 저장 + +**보상 통일**: 모든 보상은 재화 단위. 비재화 보상(칭호·스킨·장비·PC 등) 없음. + +--- + +## 4. 핵심 API 3종 + +### 4-1. `Save_StageResult` (복원 필수 — 샘플) + +**유형**: CloudScript 함수 / **호출 권한**: Client (LoginSession 유효) +**역할**: 스테이지 클리어 결과를 저장하고 재화를 지급 + +**요청 파라미터** (예시): +```json +{ + "difficulty": 2, + "chapter": 3, + "stageId": 47, + "starCount": 3, + "clearTimeMs": 125000, + "droppedItems": [{"itemId": 101, "amount": 500}], + "is_abuse_flag": false +} +``` + +**응답 (성공)**: +```json +{ + "status": "ok", + "grantedRewards": [{"itemId": 101, "amount": 500}], + "newStar": 3, + "stageProgress": {"maxStageId": 47} +} +``` + +**응답 (실패)**: +| 에러 코드 | 조건 | 클라 처리 | +|----------|------|----------| +| `AbuseFlagged` | `is_abuse_flag: true` 수신 | 지급 거부, 기록 저장 (섹션 5) | +| `SessionInvalid` | `Save_IngameStart` 세션 없음·만료 | 재로그인 유도 | +| `StageLocked` | 해당 스테이지 미해금 | 경로 이탈 처리 | + +### 4-2. `Grant_MissionReward` +- 미션 보상 재화 지급. 클라가 미션 완료 판정 후 수령 요청 시 서버가 지급·응답. +- `is_abuse_flag` 동일 수용 (클라가 자체 판정 시 true 동봉 가능). + +### 4-3. 랭킹 등록 경로 (신규 설계) +- 현 코드에서 등록 호출처 미확인 → 인간 개발자가 재구축. 엔드포인트명 예: `Submit_RankingEntry`. +- 클라 전송 점수·메타정보 그대로 Leaderboard 저장. 세부는 D-3 설계 문서에서 확정. + +--- + +## 5. 참고: 어뷰징 방지 체계 (클라 주도, 서버 최소 역할) + +> **2026-04-17 PD님 직접 재확정**: 어뷰징 **판정은 클라이언트 100% 책임**. 서버는 판정 결과(플래그)만 받아 처리. + +**판정 책임**: 클라이언트 (경계값 보관·수치 검증 전부 클라에서 수행) + +**서버 역할 (최소)**: +- 재화 지급 요청 페이로드에 **`is_abuse_flag: true`** 가 포함된 경우, 해당 지급을 **거부**하고 **기록** 저장 +- 플래그 저장 위치: PlayFab Profile 필드 또는 별도 모니터링 엔드포인트 (배정 시 결정) +- 관리자 알림 채널(Slack Webhook·이메일 등) 연계는 배정 후 결정 + +**서버가 하지 않는 것**: +- 경계값 테이블 보관 (Title Data 적재 불필요) +- 경계값과 클라 전송 수치 비교 검증 +- 어뷰징 판정 로직 자체 + +**세부 설계**: 기획팀 `공유/소통/기획팀→PM/2026-04-17_어뷰징판정_솔루션_기획서_v1.md` 참조 (클라이언트팀 구현 시 필요 시 서버 개발자와 **플래그 인터페이스만** 협의). + +--- + +## 6. 환경 셋업 체크리스트 + +- [ ] PlayFab 계정 접근 권한 수령 (Title ID 인수·Game Manager 대시보드·CloudScript revision 접근) +- [ ] 현 배포 CloudScript 전문 dump → git 편입 (경로는 PM 협의, 후보: `코어코드/수상한잡화점_서버/`) +- [ ] Unity 프로젝트 clone + `git fetch origin && git pull` (C30 준수) +- [ ] Unity 에디터 설치 (프로젝트 버전 기준) +- [ ] PlayFab SDK 연동 확인 (빌드·실행 1회 성공) +- [ ] IDE (VS Code 또는 JetBrains Rider) + Node.js (CloudScript 로컬 테스트용) + +--- + +## 7. 결정 대기 2건 (PD·PM 후속) + +1. **PD-③ 서버 스택 유지 여부** — PlayFab 유지(A안, 본 지시서 전제) vs 하이브리드(B안) vs 자체 서버(C안). 인간 개발자 배정 후 기술 선호·경험 수렴하여 PM 경유 PD님 보고. +2. **PD-④ 세이브 SOT 전환 범위** — 현 "로컬 1차 + 클라우드 보조" 하이브리드 유지(A안) vs 서버 1차 SOT(B안). 개발팀 의견: 수상한잡화점 현 시점 A안, 차기 프로젝트 B안 (코어 프레임워크 반영). + +**PD-⑤ 일일/주간 리셋 시간 기준** — 서버 시간 기준 전환 확정 (개발팀 재량, [필수 작업]). + +--- + +## 8. 용어집 + +| 용어 | 뜻 | +|------|---| +| PlayFab | Microsoft BaaS. 본 프로젝트 서버 스택 기반 | +| CloudScript | PlayFab 서버 사이드 JavaScript 실행 환경 | +| Title Data | PlayFab Title 단위 공용 설정 저장소 | +| UserData | PlayFab 유저별 서버 저장소 | +| Leaderboard | PlayFab 순위표 서비스 (Progression API) | +| ObscuredType | `CodeStage.AntiCheat` 로컬 변조 방어 타입. 서버 검증 대체재 아님 | +| ServerData | 수상한잡화점 프로젝트 클래스(`ServerClass.cs`). 유저 데이터 SOT 로컬 표현 | +| SOT | Source of Truth. 데이터의 단일 진실 근원 | +| AntiCheat | 어뷰징·치트 방지 체계. 본 프로젝트는 **클라 판정** + **서버는 플래그 수신 시 지급 거부** | + +--- + +## 9. 기각안 (P24·P27 결정·설계 엔트리 필수) + +1. **"어뷰징 경계값 서버 보관·검증" 안 (v1.0 B-7 구조)** — PD님 재결정으로 기각. 서버가 경계값 테이블을 Title Data에 적재하고 수치 검증하는 구조는 폐기. 어뷰징은 **클라 주도 작업**이며 서버 개발자 작업 스펙 아님. +2. **"상세 설계 전부 포함한 장문 지시서" 안 (v1.0 446줄)** — 인간 개발자 파악 시간 낭비. 요약판 원칙으로 전환 (PD님 지시). +3. **"비재화 보상 유지" 안** — PD 확정(모든 보상=재화)으로 전제 소멸. 기각. +4. **"PlayFab 전면 폐기 + 자체 서버 전면 신규" 안** — 현 보류 항목 해소 전 범위 확대 리스크. PD-③ 옵션으로만 제시. +5. **"배정 전 세부 API 스펙 전량 확정" 안** — 배정된 개발자의 기술 선호 반영 없는 스펙은 재작업 리스크. 본 지시서는 원칙·경계·샘플 1건까지, 세부는 배정 후 설계 문서에서 확정. + +--- + +## 10. 변경 이력 + +| 일시 | 버전 | 작성자 | 요지 | +|------|------|--------|------| +| 2026-04-17 | v1.0 | 개발팀장 | 최종본 초판 (B-7 어뷰징 방지 서버 연계 포함). PD님 어뷰징 책임 재확정으로 대체됨 | +| 2026-04-17 | **v1.1** | 개발팀장 | **요약판 재작성**: (1) 어뷰징 판정 클라 100% 책임 확정 반영 — 서버는 플래그 수신만 (섹션 5). (2) 446줄 → 150줄 이내 축약. (3) 샘플 API 1건 유지, 템플릿·매트릭스 세부는 D-3 설계 문서로 이관 | + +--- + +**서명**: 개발팀장 (인간 서버 개발자 배정 시 본 지시서를 출발점으로 사용) +**DOCX 변환**: PM이 `anthropic-skills:docx`로 재생성 +**후속 트래킹**: 개발팀 PD 지시 로그 #30 v1.1 갱신 + #신규 "어뷰징 책임 재확정·요약판 재작성" 등록·완료 diff --git a/공유/소통/기획팀→PM/2026-04-17_어뷰징판정_솔루션_기획서_v1.md b/공유/소통/기획팀→PM/2026-04-17_어뷰징판정_솔루션_기획서_v1.md new file mode 100644 index 0000000..5a69bc2 --- /dev/null +++ b/공유/소통/기획팀→PM/2026-04-17_어뷰징판정_솔루션_기획서_v1.md @@ -0,0 +1,378 @@ +--- +type: 기획서 +project: 수상한잡화점 +version: v1 +status: 진행중 +author: 기획팀장 +date: 2026-04-17 +scope: 어뷰징 판정 프레임워크 (설계 원칙 + 스키마 + 예시 틀) +related_rules: [C7, C11, C13, C23, P23] +related_docs: + - 공유/소통/기획팀→PM/2026-04-17_Unity_MCP_시뮬레이션_기획검토_기획팀.md + - 프로젝트/수상한잡화점/기획/Phase3_재개준비_체크리스트_v1.md +--- + +# 어뷰징 판정 솔루션 기획서 v1 + +## 요지 + +스테이지 클리어 보상·랭킹 등록·일일 미션에서 클라가 전송하는 데이터를 서버가 **"시뮬레이터 산정 이론 상한 경계값"** 기반으로 검증하여, 경계를 초과하는 요청을 어뷰저로 판정·차단한다. 기획팀은 경계값 도출 방법론과 테이블 스키마를 제공하고, 서버는 검증 로직을 탑재한다. 실제 수치는 Unity MCP 시뮬 가동 결과로 채워진다(본 기획서는 틀만 제공). + +**재미 관점(C7)**: 어뷰징이 방치되면 랭킹·미션의 도전 가치가 붕괴되고, 정상 플레이어의 성취감이 훼손된다. 본 솔루션의 목표는 "정직한 플레이어의 재미를 보호"하는 것이지 선의의 실력자를 잡는 것이 아니다. 따라서 경계값은 **이론 상한에 안전 마진을 더한 값**으로 설정하여 false positive를 최소화한다. + +--- + +## A. 문제 정의 + +### A-1. 전제 +- 모든 보상(미션·스테이지·랭킹) = 재화 형태로 지급 확정 (2026-04-17 PD님 결정) +- 스테이지 클리어·랭킹 등록에서 서버는 클라 전송 데이터를 기반으로 보상을 산정/기록한다 +- 클라를 무조건 신뢰하면 메모리 조작·패킷 위변조를 통한 재화 부당 획득이 가능하다 + +### A-2. 어뷰징 공격 벡터 + +| 벡터 ID | 대상 액션 | 조작 포인트 | 파급 위험 | +|--------|----------|-----------|---------| +| **AV-1** | 스테이지 클리어 완료 패킷 | 클리어타임 단축 (예: 0초·음수) | 시간 가속 보상 파밍 | +| **AV-2** | 스테이지 클리어 완료 패킷 | 획득 재화량 조작 (상한 초과) | 재화 무한 생성 | +| **AV-3** | 스테이지 클리어 완료 패킷 | 별(★) 개수 조작 (3/3 고정) | 미달성 보상 부당 수령 | +| **AV-4** | 스테이지 클리어 완료 패킷 | 소모 자원 0 보고 (물약·쉴드·HP) | ★조건 달성 위조 (C6 포션절제, N2 피격제한 등) | +| **AV-5** | 랭킹 등록 패킷 | 점수값 조작 (이론 상한 초과) | 랭킹 1위 탈취 | +| **AV-6** | 랭킹 등록 패킷 | 연관 메타값 조작 (사용 카드·편성 등 검증 소재) | 검증 회피 | +| **AV-7** | 일일 미션 카운트 | 카운트 중복 전송·상한 초과 | 일일 한도 초과 보상 수령 | +| **AV-8** | 리플레이/재접속 재전송 | 동일 클리어 결과 재제출 | 중복 보상 | +| **AV-9** | 스테이지 잠금 우회 | 언락 안 된 스테이지 완료 패킷 | 진행도 건너뛰기 | + +### A-3. 판정 대상 범주 3종 + +1. **물리적 불가능** — 시뮬레이터로도 달성 불가능한 수치 (예: 클리어타임 0초, 점수 int.MaxValue) +2. **확률적 극단** — 이론상 가능하나 확률이 충분히 낮아 사실상 어뷰징으로 간주 (예: 운으로 1회 나올 수치의 연속 달성) +3. **논리적 모순** — 내부 값들이 서로 모순 (예: "피격 0회"인데 "HP 50% 소진") + +--- + +## B. 경계값 도출 방법론 + +### B-1. 기본 공식 + +``` +경계값 = 시뮬_이론_극값 × 안전마진계수 +``` + +- `시뮬_이론_극값`: Unity MCP 시뮬을 N회(권장 N=1,000 이상) 수행하여 얻은 **최상위 극값** (최대/최소는 벡터에 따라 다름) +- `안전마진계수`: 벡터 특성에 따라 결정하는 완화 계수 (정상 플레이어 오차 허용) + +### B-2. 안전마진 설계 원칙 + +| 벡터 특성 | 마진 방향 | 계수 예시 | 근거 | +|---------|--------|---------|------| +| 클리어타임 (짧을수록 의심) | 하한 경계값 = 이론 최단 × **0.80** | 0.80 | 프레임 드랍·클라 시계 오차로 정상 유저도 5~15% 짧게 보고 가능 | +| 획득 재화 (많을수록 의심) | 상한 경계값 = 이론 최대 × **1.05** | 1.05 | 소숫점 반올림·버프 중첩 경계 케이스 흡수 | +| 랭킹 점수 (높을수록 의심) | 상한 경계값 = 이론 최대 × **1.10** | 1.10 | 신규 카드 출시 등 업데이트 이후 시뮬 갱신 전 과도기 흡수 | +| 피격 수 (적을수록 의심) | 하한 경계값 = 이론 최소 (= 0) | - | 0회도 이론상 가능, 음수만 차단 | +| 미션 카운트 | 상한 경계값 = 일일 최대 달성 가능치 × **1.00** | 1.00 | 마진 불필요 (정수 카운트) | + +**주의**: 마진은 "정상 유저를 어뷰저로 오판정하지 않기 위한" 안전장치지, "어뷰저를 놓치는 관용"이 아니다. 극값의 80%~110% 범위는 경계이므로 이 범위 유저는 "**1차 통과 + 플래그 미부여**"로 처리한다. + +### B-3. 업데이트 주기 + +1. **정기 갱신**: 밸런스 패치(카드 수치 변경·신규 몬스터 추가) 시 해당 영역 시뮬 재실행 +2. **긴급 갱신**: 어뷰징 신고 접수 또는 랭킹 이상치 감지 시 즉시 시뮬 재실행·경계값 조정 +3. **버전 관리**: 경계값 테이블은 C6에 따라 `.bak_{YYYYMMDD_HHMM}.json` 백업 후 교체 +4. **C13 기록**: 경계값 변경은 PD 지시 로그·대화로그에 기각안 포함 기록 (P24) + +### B-4. 시뮬레이터 입력 조건 (Unity MCP) + +각 스테이지·각 벡터에 대해 다음 조건의 최극단 조합을 시뮬: +- 캐릭터: 최강 편성 (풀레벨·최적 카드) +- 적 패턴: 최선 운 (치명타 확률 최대·적 회피 최소) +- 플레이 입력: 완벽 입력 (반응시간 0·공백 없음) +- 물약·쉴드: 최대 활용 (획득 재화 벡터) / 미사용 (C6 조건 벡터) + +**출력 수집**: 10,000회 반복하여 상위/하위 0.1% 값을 "이론 극값"으로 채택 (절대 최대/최소 아닌 이유 = 시뮬 자체 오차 흡수). + +--- + +## C. 검증 계층 설계 + +### C-1. 2계층 검증 원칙 + +**1계층 (클라)** — 예방적 차단 +- 목적: 악의적 조작이 아닌 **버그·의도치 않은 극값**을 클라 단에서 거르기 +- 클라가 자체 생성한 결과값이 경계값을 초과하면 전송 보류 + 경고 로그 수집 +- **클라 신뢰는 하지 않음** — 이 계층만으로는 판정 금지 (메모리 조작 시 이 계층 무력화 가능) + +**2계층 (서버)** — 최종 판정 +- 목적: 모든 어뷰징 판정의 **단일 결정권자** +- 서버가 경계값 테이블을 보유하고 수신 패킷 전량 검증 +- 경계 초과 시: 거부 + 플래그 기록 + (심각도에 따라) 관리자 알림 + +### C-2. 검증 흐름 (서버) + +``` +[클라 패킷 수신] + ↓ +[Step 1] 기본 유효성 (음수·null·형식 오류) → 거부 + ↓ +[Step 2] 스테이지 잠금 상태 확인 (AV-9 대응) → 거부 + ↓ +[Step 3] 중복 제출 확인 (AV-8 대응, 같은 세션ID·클리어ID) → 거부 + ↓ +[Step 4] 경계값 테이블 조회 → 각 필드 범위 검증 + ↓ (통과) +[Step 5] 논리 정합성 검증 (필드 간 모순, A-3-3 범주) + ↓ (통과) +[정상 처리 + 보상 지급] + + ↓ (Step 4·5 실패 시) +[플래그 기록 + 보상 보류 + 심각도별 조치] +``` + +### C-3. 역할 분담 원칙 + +| 주체 | 역할 | 금지 | +|-----|-----|-----| +| 클라 | 버그 조기 감지·UX 경고 | 최종 판정, 플래그 기록 | +| 서버 | 최종 판정, 플래그 DB 기록, 관리자 알림 | 클라 통과 가정·검증 생략 | +| 기획팀 | 경계값 테이블 공급 | 서버 검증 로직 직접 작성 | +| 인간 서버 개발자 | 검증 로직 구현·플래그 DB 스키마 | 경계값 자체 수정 (기획 승인 없이) | + +--- + +## D. 어뷰저 플래그 체계 + +### D-1. 플래그 종류 3단계 + +| 단계 | 트리거 | 자동 조치 | 관리자 조치 | +|-----|-------|---------|-----------| +| **F1 경미** | 경계값의 +5%~+10% 범위 이내 초과 (마진 범위 경계) | 플래그 기록만 | 누적 3회 이상 시 F2 격상 | +| **F2 중대** | 경계값의 +10%~+50% 초과, 또는 F1 3회 누적 | 해당 보상 보류 + 관리자 알림 | 수동 검토 후 재화 회수·경고 | +| **F3 확정** | 경계값의 +50% 초과, 논리 모순 발생, 또는 F2 2회 누적 | 해당 보상 미지급 + 계정 플래그 | 계정 정지 검토 (랭킹 삭제 포함) | + +### D-2. 플래그 누적 처리 + +- 플래그 DB: `{user_id, timestamp, vector_id, flag_level, raw_data, boundary_value, reason}` +- 기간 윈도우: 최근 30일 기준 누적 (기획팀 권고안, 서버측 조정 가능) +- 에스컬레이션: F1×3 → F2 / F2×2 → F3 / F3×1 → 즉시 계정 정지 검토 대상 + +### D-3. 기획팀 권고안 (PM 경유 운영팀 협의 필요) + +- **F1**: 자동 회수 없음 (false positive 위험). 사용자에게 경고도 노출하지 않음 (어뷰저 학습 방지) +- **F2**: 해당 회차 보상만 회수. 사용자에게는 "서버 동기화 오류, 재시도" 메시지 노출 +- **F3**: 계정 정지는 관리자 최종 판단. 랭킹 등록 데이터는 즉시 삭제 + +--- + +## E. 대상 액션별 구체 경계값 초안 + +> **주의**: 아래 수치는 모두 **플레이스홀더**. Unity MCP 시뮬 가동 후 확정. 실제 경계값 테이블은 별도 JSON으로 관리. + +### E-1. 스테이지 클리어 + +| 필드 | 검증 규칙 | 경계값 예시 (Stage 1) | +|-----|---------|----------------------| +| `clearTime` | `sim_min × 0.80` 하한, `sim_max × 1.20` 상한 | 하한: 미확정(시뮬 후) / 상한: 미확정 | +| `goldEarned` | `sim_max × 1.05` 상한 | 상한: 미확정(시뮬 후) | +| `starsEarned` | 0~3 정수, ★조건 메타값과 교차 검증 | 0~3 | +| `hpRemaining` | `0 ≤ hp ≤ maxHp` | 캐릭터 maxHp 기반 | +| `potionUsed` | 0 이상 정수, 소지 한도 이하 | 캐릭터 포션 한도 | +| `shieldRemaining` | `0 ≤ shield ≤ maxShield` | 캐릭터 maxShield 기반 | +| `hitsTaken` | 0 이상 정수, `sim_max × 1.20` 상한 | 상한: 미확정(시뮬 후) | + +### E-2. 랭킹 등록 + +| 필드 | 검증 규칙 | 경계값 예시 | +|-----|---------|----------| +| `score` | `sim_max × 1.10` 상한 | 상한: 미확정(시뮬 후) | +| `clearTime` | 동일 스테이지 E-1 규칙 적용 | - | +| `deckComposition` | 소유 카드 목록과 교차 검증 | - | +| `stageId` | 유효 스테이지 ID (잠금 해제 상태) | - | + +### E-3. 일일 미션 + +| 필드 | 검증 규칙 | 경계값 예시 | +|-----|---------|----------| +| `missionId` | 유효 미션 ID + 당일 활성 | - | +| `currentCount` | `missionMaxCount` 상한 | 미션별 상한 | +| `incrementDelta` | 단일 트리거로 1 증가만 허용 | 1 | +| `completionTimestamp` | 일일 리셋 시각 이후 ~ 현재 | - | + +### E-4. ★조건 교차 검증 (P17 연계) + +각 스테이지의 활성 ★조건(C1~C9, N1~N6)과 클리어 결과 필드가 정합해야 한다. 예: +- **C6 (포션 절제)**: `potionUsed == 0` 인지 확인 +- **N2 (피격 제한)**: `hitsTaken ≤ 서브맵수 × 1` 인지 확인 +- **N4 (쉴드 하한 30%)**: `shieldRemaining ≥ maxShield × 0.30` 인지 확인 + +★조건 통과 보고와 필드 값이 모순되면 **F3 확정 플래그** (논리 모순). + +--- + +## F. 서버 측 요구사항 (개발팀 인계용) + +### F-1. 기획팀 공급 산출물 + +1. **경계값 테이블** — JSON 파일 (Unity MCP 시뮬 결과 반영) +2. **★조건 검증 규칙** — 스테이지별 활성 조건 + 판정 로직 +3. **플래그 기준** — F1/F2/F3 경계 정의 +4. **갱신 공지** — 경계값 변경 시 개발팀에 PR 또는 소통 채널 통지 + +### F-2. 경계값 테이블 스키마 (JSON 예시) + +```json +{ + "version": "1.0.0", + "generatedAt": "2026-04-17T00:00:00Z", + "simulationRuns": 10000, + "stages": { + "stage_01": { + "clearTime": { + "min": 42.3, + "max": 180.0, + "marginLow": 0.80, + "marginHigh": 1.20, + "boundaryMin": 33.84, + "boundaryMax": 216.0 + }, + "goldEarned": { + "max": 250, + "marginHigh": 1.05, + "boundaryMax": 263 + }, + "hitsTaken": { + "min": 0, + "max": 8, + "marginHigh": 1.20, + "boundaryMax": 10 + }, + "starConditions": { + "C2": { "field": "hitsTaken", "operator": "==", "value": 0 }, + "C6": { "field": "potionUsed", "operator": "==", "value": 0 }, + "N2": { "field": "hitsTaken", "operator": "<=", "formula": "subMapCount * 1" } + } + } + }, + "ranking": { + "stage_01": { + "score": { "max": 12500, "marginHigh": 1.10, "boundaryMax": 13750 } + } + }, + "dailyMissions": { + "login": { "maxCount": 1 }, + "clear_any_stage": { "maxCount": 10 } + } +} +``` + +### F-3. 검증 API 호출 흐름 (서버 내부) + +``` +수신: POST /api/stage/complete + body: { userId, stageId, clearTime, goldEarned, starsEarned, hitsTaken, potionUsed, ... } + +서버 처리: + 1. AbuseValidator.validate(userId, stageId, body) + → boundaries = BoundaryTable.get(stageId) + → for each field: 경계값 비교 + → starCondition 교차 검증 + → 중복 제출 체크 + 2. ValidationResult: + - PASS: 보상 지급 처리 + - F1: 보상 지급 + 플래그 기록 + - F2: 보상 보류 + 플래그 기록 + 관리자 알림 + - F3: 보상 미지급 + 플래그 기록 + 계정 플래그 + 3. 응답: + - PASS/F1: { status: "success", rewards: [...] } + - F2: { status: "pending", message: "서버 동기화 오류, 재시도 바랍니다" } + - F3: { status: "blocked" } (어뷰저 학습 방지 위해 구체 사유 미노출) +``` + +### F-4. 위반 응답 포맷 (권고) + +어뷰저 학습 방지를 위해 구체 수치·사유를 클라에 노출하지 않는다: + +```json +// F1 (경미) +{ "status": "success", "rewards": [...] } // 플래그는 서버 내부만, 클라 무감지 + +// F2 (중대) +{ "status": "pending", "message": "서버 동기화 오류, 잠시 후 재시도" } + +// F3 (확정) +{ "status": "blocked" } +``` + +### F-5. 플래그 DB 스키마 (서버 구현 시 참조) + +``` +AbuseFlag: + - id: UUID + - userId: string + - timestamp: datetime + - vectorId: enum (AV-1 ~ AV-9) + - flagLevel: enum (F1, F2, F3) + - stageId: string (nullable) + - fieldName: string + - rawValue: json + - boundaryValue: json + - exceedRate: float // (rawValue - boundary) / boundary + - reason: string + - reviewedBy: string (nullable) + - reviewedAt: datetime (nullable) + - resolution: enum (pending, confirmed_abuse, false_positive, pardoned) +``` + +--- + +## G. 기각안 + +### G-1. 서버 단독 시뮬 재현 검증 (채택 안 함) + +- **안**: 서버가 클라 입력(키·타이밍)을 받아 서버에서 전투를 재시뮬하여 결과 일치 여부 검증 +- **기각 사유**: (1) 서버 CPU 비용 폭증 — 1회 재시뮬 ≈ 1개 전투 전체 연산 (2) 동기화 이슈 (난수 시드·부동소수점) (3) 현 프로젝트가 실시간 멀티플레이가 아닌 싱글 플레이 구조라 과잉 투자. **채택 안**인 "경계값 기반 통계적 검증"이 비용·구현 난이도·효과 3축에서 우수. + +### G-2. 머신러닝 기반 이상치 탐지 (채택 안 함) + +- **안**: 유저 플레이 데이터를 수집하여 ML 모델로 어뷰저 이상치 자동 탐지 +- **기각 사유**: (1) 학습 데이터 축적에 운영 기간 필요 — 초기 출시 대응 불가 (2) false positive 조정 난해 (3) 인간 서버 개발자 부담 가중. 장기적 보조 수단으로는 고려 가치 있으나 v1 범위에서 제외. 본 기획서는 시뮬 경계값으로 출시 대응 + 운영 후 데이터 축적 → 차후 ML 도입 여지 유지. + +### G-3. 클라 단독 판정 (채택 안 함) + +- **안**: 클라가 자체 검증 후 서버에는 결과만 전송 +- **기각 사유**: 메모리 조작·패킷 위변조로 무력화됨. 어뷰징 방지 근본 목적 미달성. **원천적으로 고려 대상 아님**. + +### G-4. 전체 플레이 로그 전송·서버 기록 (채택 안 함) + +- **안**: 매 프레임 입력 로그 전체를 서버로 전송하여 서버가 사후 감사 +- **기각 사유**: (1) 네트워크 비용 과도 (2) 저장소 비용 (3) G-1과 유사하게 오버엔지니어링. F3 확정 플래그 시점에 사후 감사용으로 마지막 30초~60초 정도만 보관하는 방안은 서버측 재량. + +### G-5. 안전 마진 0% 엄격 검증 (채택 안 함) + +- **안**: 시뮬 극값을 그대로 경계값으로 사용하여 극한 배제 +- **기각 사유**: false positive 폭발. 프레임 드랍·반올림 오차로 정상 유저도 매번 플래그됨. **C7 재미 우선 원칙 위반** — 정상 플레이어 경험 훼손. 5%~20% 마진 보정 필수. + +--- + +## 후속 작업 (Unity MCP 시뮬 가동 후 수행) + +1. 각 스테이지·각 벡터별 시뮬 10,000회 실행 +2. 경계값 테이블 JSON 1차 산출 (v1.0.0) +3. balance-designer 검토 → 재미 관점 마진 재조정 +4. 개발팀과 스키마·API 계약 확정 (`공유/소통/기획팀→개발팀/` 발행) +5. 플래그 DB 스키마 서버팀 리뷰·구현 +6. 통합 QA 시 어뷰저 시뮬 툴로 F1/F2/F3 각각 검증 케이스 생성 + +## 연관 규칙·문서 + +- **C7**: 재미 우선 — 정상 플레이어 보호를 위한 마진 설계 근거 +- **C11**: 개발 관점 — 서버 자원 효율 고려하여 재시뮬 대신 경계값 비교 채택 (G-1 기각 근거) +- **C13**: PD 지시 트래킹 — 본 기획 등록 및 경계값 변경 이력 관리 +- **C23**: 허위 보고 금지 — 본 기획서는 실제 시뮬 가동 전이므로 수치는 모두 플레이스홀더 명시 +- **P17**: ★조건 배타 배치 — E-4 ★조건 교차 검증과 연계 +- **P23**: 기획 결정 재량 — 본 기획은 기획팀장 재량 범위(신규 시스템 제안은 PM 확인 필요 → 본 기획서 발행으로 PM 확인 경유) + +--- + +**기획팀장 발의** +**PM 검토 요청**: 본 기획서 스키마·프레임워크 승인 → 개발팀 F 섹션 인계 트리거 +**승인 후 착수**: Unity MCP 시뮬 가동 (별도 PD 지시 로그) diff --git a/공유/소통/기획팀→개발팀/REQ-템플릿_밸런스수치.md b/공유/소통/기획팀→개발팀/REQ-템플릿_밸런스수치.md new file mode 100644 index 0000000..0b0350f --- /dev/null +++ b/공유/소통/기획팀→개발팀/REQ-템플릿_밸런스수치.md @@ -0,0 +1,142 @@ +--- +요청번호: REQ-XXX (일련번호 부여) +요청일: YYYY-MM-DD +요청부서: 기획팀 +수신부서: 개발팀 +담당에이전트: (예: /게임플레이, /클라이언트) +우선순위: HIGH | MID | LOW +상태: 대기 | 진행중 | 응답완료 | 보류 +유형: 밸런스수치_요구서 +관련PD지시: #N (해당 시) +--- + +# 밸런스 수치 요구서 표준 템플릿 + +> **용도**: 기획팀이 개발팀에 밸런스 수치 변경·신규 테이블 반영을 요청할 때 사용하는 표준 포맷. +> **원칙**: C7(재미 근거 필수) · C11(개발 관점 존중) · C6(백업 의무) · P16(변경 이력) · P24(기각안 기록). +> **사용법**: 본 파일을 복사하여 `YYYY-MM-DD_REQ-XXX_요약제목.md`로 저장 후 채워 넣는다. + +--- + +## 1. 요구서 식별 + +| 필드 | 값 | +|------|-----| +| **요구서 ID** | REQ-XXX | +| **기준 버전** | (예: PCAwakening.json `v1.3.2`, 수상한잡화점_밸런싱전략_v1.md) | +| **기준 커밋** | (git SHA 또는 "main@YYYY-MM-DD HH:MM") | +| **작성자** | (기획팀장 / balance-designer 등) | + +--- + +## 2. 변경 필드 목록 + +대상 파일·테이블과 변경 대상 필드를 명시. + +| # | 파일·테이블 | 필드 경로 | 변경 유형 | +|---|------------|---------|---------| +| 1 | (예: `table_PCAwakening.json` > PC6001) | `s_Value` | 수정 | +| 2 | (예: `카드시너지_v2.xlsm` > Sheet1!B5:B20) | `effect_coefficient` | 신규 | + +**변경 유형**: `수정` / `신규` / `삭제` / `구조변경` + +--- + +## 3. 변경 전후 수치 + +실제 수치를 전후 대비로 명시. 다건이면 표로, 단건이면 단락으로. + +| # | 대상 | 현재값 | 제안값 | 비고 | +|---|------|--------|--------|------| +| 1 | PC6001 MaxHP Node 100394 `s_Value` | `'500%'` | `'50%'` | 데이터 입력 오류 정정 | +| 2 | 스테이지 5 몬스터 HP | 1200 | 1400 | 난이도 곡선 조정 | + +성장 곡선·구간 변경 시 구간별 전후 값 전수 명시. + +--- + +## 4. 재미 근거 (C7 필수) + +> **"어떤 재미를 강화하는가"를 먼저 정의**해야 수치 변경이 허용된다 (C7). 재미 정의 없는 수치 조정은 금지. + +- **강화하려는 재미 축**: (예: "보스전 긴장감", "빌드 다양성", "만렙 성취감") +- **변경 전 재미 문제점**: (예: "현재 만렙 DPS +1067%로 보스가 3초 내 클리어되어 긴장감 소실") +- **변경 후 기대 경험**: (예: "만렙 DPS +50% 수준으로 보스 TTK 10~15초 구간 유지, 긴장감 보존") +- **측정 지표**: (예: "Unity MCP 시뮬 100회 평균 TTK", "만렙 클리어율 70~85% 구간") + +--- + +## 5. 개발 관점 우려 예상 (C11 존중) + +> 기획팀이 개발팀 관점(C11 — 자원 효율성·코드 직관성·범용성)에서 예상되는 우려를 **사전 명시**하여 논의 효율을 높인다. 개발팀이 추가 우려를 발견 시 C3(은폐 금지)에 따라 즉시 제기. + +| 관점 | 예상 우려 | 기획팀 입장 | +|------|----------|-------------| +| **자원 효율성** | (예: 매 프레임 재계산으로 CPU 부담) | (예: 변경 빈도 낮음 — 초기화 시 1회 계산 가능) | +| **코드 직관성** | (예: `500%` vs `5.0` 혼재로 파싱 복잡) | (예: 신규 `s_Value_type` 컬럼 도입 제안) | +| **범용성** | (예: 차기 프로젝트 재활용 어려움) | (예: 프로젝트 특수 로직으로 한정, 범용 모듈 분리) | + +우려 없을 시 "없음 (기획팀 분석 범위 내 우려 없음)" 명시. + +--- + +## 6. 검증 방법 + +변경이 의도대로 동작하는지 확인할 수 있는 검증 시나리오. + +- **검증 채널**: Unity MCP 시뮬 / 로컬 빌드 / QA 수동 / 수치 단위 테스트 +- **검증 케이스**: + 1. (예: "만렙 PC 5종에 각성 트리 풀 해방 상태로 Stage 10 보스 진입 → 10회 반복 → TTK 평균 10~15초 확인") + 2. (예: "중간 단계 3레벨 상태 전투 시뮬 → DPS 증가율 +20~30% 구간 확인") +- **통과 기준**: (검증 케이스별 수치 기준 명시) +- **회귀 검증**: 변경 대상 외 기존 밸런스 경로(P14 QA 게이트) 영향 없음 확인 + +--- + +## 7. 백업·이력 (C6·P16) + +- **백업 파일**: `{원본명}.bak_YYYYMMDD_HHMM.{확장자}` — 개발팀이 변경 착수 전 생성 +- **변경 이력 기록 위치**: 대상 md 문서 하단 "변경 이력 테이블"에 append +- **관련 대화로그**: `공유/대화로그/수상한잡화점/YYYY-MM-DD.md` 엔트리 링크 + +--- + +## 8. 기각안 (P24 — 결정성 요청이면 권장) + +검토했으나 채택하지 않은 대안과 기각 사유. 조직 자산 축적의 핵심 (헌법 제1원칙 목표 2 원칙 B). + +| # | 기각 대안 | 기각 사유 | +|---|----------|----------| +| 1 | (예: 현재값 유지 + 별도 난이도 옵션 도입) | (예: 신규 시스템 비용 대비 재미 개선 불확실) | +| 2 | (예: `500%` 일괄 → `100%`) | (예: 과도한 하향으로 각성 성취감 소실 우려) | + +기각 대안 미검토 시 "없음 (단일안 — 사유: ...)" 명시. 공란 금지. + +--- + +## 9. 응답 섹션 (개발팀 작성) + +> 개발팀이 본 요구서에 응답할 때 아래 섹션을 append. 별도 응답 파일 분리 불필요 (본 파일 단일 SOT). + +### 9-1. 개발 관점 검토 결과 +- 수용 가능 여부: 수용 / 조건부 수용 / 반려 +- 추가 우려: (기획팀 예상 외 발견 시) +- 대안 제시: (반려·조건부 시) + +### 9-2. 변경 적용 결과 +- 적용 커밋: (SHA) +- 백업 경로: (`.bak_YYYYMMDD_HHMM.{확장자}`) +- QA 검증 결과: (통과/실패 + 상세) +- 회귀 검증: (통과/실패) + +### 9-3. 후속 필요 작업 +- (예: 밸런스 조정 후 balance-designer 재튜닝 필요) +- (예: Unity MCP 시뮬 재실행 필요 — 경계값 재산출) + +--- + +## 변경 이력 + +| 일시 | 변경자 | 변경 내역 | +|------|--------|----------| +| 2026-04-17 | 기획팀장 | 표준 템플릿 신설 (PD님 직접 지시, 팀장 재량 진행 승인) | diff --git a/공유/소통/개발팀→PM/2026-04-16_RPT_시뮬레이션_대응_현황보고.md b/공유/소통/완료/2026-04-16_RPT_시뮬레이션_대응_현황보고.md similarity index 100% rename from 공유/소통/개발팀→PM/2026-04-16_RPT_시뮬레이션_대응_현황보고.md rename to 공유/소통/완료/2026-04-16_RPT_시뮬레이션_대응_현황보고.md diff --git a/공유/소통/개발팀→PM/2026-04-16_업무현황_개발실.md b/공유/소통/완료/2026-04-16_업무현황_개발실.md similarity index 100% rename from 공유/소통/개발팀→PM/2026-04-16_업무현황_개발실.md rename to 공유/소통/완료/2026-04-16_업무현황_개발실.md diff --git a/공유/소통/기획팀→PM/2026-04-16_업무현황_기획실.md b/공유/소통/완료/2026-04-16_업무현황_기획실.md similarity index 100% rename from 공유/소통/기획팀→PM/2026-04-16_업무현황_기획실.md rename to 공유/소통/완료/2026-04-16_업무현황_기획실.md diff --git a/공유/소통/개발팀→PM/2026-04-16_콘솔병렬실행_기술검토_개발팀.md b/공유/소통/완료/2026-04-16_콘솔병렬실행_기술검토_개발팀.md similarity index 100% rename from 공유/소통/개발팀→PM/2026-04-16_콘솔병렬실행_기술검토_개발팀.md rename to 공유/소통/완료/2026-04-16_콘솔병렬실행_기술검토_개발팀.md diff --git a/공유/소통/개발팀→PM/2026-04-16_핫리로드대안_기술검토_개발팀.md b/공유/소통/완료/2026-04-16_핫리로드대안_기술검토_개발팀.md similarity index 100% rename from 공유/소통/개발팀→PM/2026-04-16_핫리로드대안_기술검토_개발팀.md rename to 공유/소통/완료/2026-04-16_핫리로드대안_기술검토_개발팀.md diff --git a/공유/소통/완료/2026-04-17_RPT_서버역할_정리_초안.md b/공유/소통/완료/2026-04-17_RPT_서버역할_정리_초안.md new file mode 100644 index 0000000..593257e --- /dev/null +++ b/공유/소통/완료/2026-04-17_RPT_서버역할_정리_초안.md @@ -0,0 +1,315 @@ +--- +from: 개발팀장 +to: PM +date: 2026-04-17 +type: 보고서(초안) +status: 진행중 +tags: [서버역할정리, 인간개발자지시서, 클라서버경계, PlayFab, 수상한잡화점] +related: [C11, C29, C30, P13, P18, P23] +depends_on: [PD-#2, PD-#5-B] +--- + +# 인간 서버 개발자 업무 지시서 초안 — 서버 역할 정리 + +> **목적**: 인간 서버 개발자가 읽고 바로 작업 착수 가능한 **서버 역할 범위·데이터 카테고리·클라/서버 처리 경계 정의서** 초안. 최종 결정은 PD님 몫이며 본 문서는 **초안 + PD 결정 안건** 제시. +> +> **작성 방법 주석 (C23 정직성)**: 본 세션에서 `Task` 도구로 서버팀장·클라이언트팀장 서브에이전트 호출이 환경 제약으로 불가하여, 개발팀장이 **양쪽 관점을 명시적으로 구분해 단독 분석**. 섹션마다 `[서버 관점]`·`[클라 관점]` 태그로 구분. 서버팀장의 사전 업무공유체계 점검 보고(`공유/소통/개발팀→PM/2026-04-17_업무공유체계_점검_서버팀.md`)는 참조함. + +--- + +## 0. 전제·근거 자료 + +### 0-1. PD님 가이드라인 (본 초안의 기본 원칙, 반드시 반영) +1. **재화 사용 시 항상 서버 응답 필수** — 클라 단독 처리 금지 +2. **미션 클리어 판단은 클라 재량**. 단 **보상 지급 주체(서버 vs 클라)는 결정 필요**. **보상이 재화 형태가 아니면 서버 처리 어려움** 전제 +3. **랭킹 등록은 클라 정보를 서버가 그대로 저장** — 서버는 검증·계산 없이 저장만. **재화 아닌 랭킹 보상은 서버 처리 불가** 전제 + +### 0-2. 현 수상한잡화점 실측 결과 + +| 항목 | 실측 내용 | +|------|----------| +| **서버 스택** | **PlayFab** (Title API + CloudScript + Leaderboard + Profiles + Mail) | +| **인증** | `LoginWithCustomID` (기기 기반 익명 ID) | +| **저장 방식** | **하이브리드** — `CryptoUtil.Save`로 **로컬 파일 저장이 1차 소스**, PlayFab CloudScript는 핵심 이벤트만 (로그인 토큰·치트·인게임 시작·가이드 퀘스트·우편) | +| **AntiCheat** | `CodeStage.AntiCheat.ObscuredTypes` (ObscuredInt/Long, RandomizeCryptoKey) — **로컬 변조 방어 목적**, 서버 검증은 아님 | +| **CloudScript 함수 (현행)** | `ServerSave_LoginToken`, `Get_UserInfo`, `Save_CheatItem`, `Save_IngameStart`, `Save_GuideQuest`, `Get_MailList`, `Get_MailReward`, `Get_AllMail`, `Get_OtherUserInfo`, `Del_TitlePlayer` | +| **CloudScript 주석처리 (미구현)** | `Save_StageResult` — 스테이지 완료 시 서버 저장. 현재 비활성 | +| **랭킹** | `PlayFabProgressionAPI.GetLeaderboard` / `GetLeaderboardAroundEntity` 사용 중 (단, 등록 경로는 소스에서 미확인 — 추가 분석 필요) | +| **인앱 결제** | `InappInfo.cs` **전체 주석처리 상태** (현재 비활성). 재활성 시 `ValidateGooglePlayPurchase` 등 PlayFab 검증 경로 복원 필요 | +| **주요 저장 데이터 (`ServerData` 클래스)** | StageData, dic_PC, dic_PC_Awakening, Seal(인장), Equipment, dic_Item(재화/소비), dic_Card, EquipmentTrade, CatGoodsShop, Mission(일일/주간), set_PurchasedPCIDs, show_Scenarios, 출석(RewardAttandanceDay), 고양이 레벨(CatLv/CatExp) | + +### 0-3. 미파악·추가 분석 필요 영역 (C23 정직성) +- **메타 시스템 SOT 미작성** — 세이브·상점·성장·시즌패스의 공식 설계 문서 부재 (PD #5-B Phase 0-C 미착수) +- **랭킹 등록 경로** — `GetLeaderboard` 읽기는 있으나 `UpdatePlayerStatistics`나 CloudScript 등록 호출처가 소스 검색으로 확인 안 됨 +- **PlayFab 계속 사용 여부** — PD님 결정 필요 (자체 서버 전환 가능성). 본 초안은 **PlayFab 유지 + CloudScript 확장** 전제로 작성 + +--- + +## A. 서버 관리 데이터 카테고리 (서버 SOT 후보) + +> **범례**: `[확정]` 현 코드 존재 + 재화성 데이터 / `[추가분석]` 현 코드에 있으나 서버 SOT 전환 검토 필요 / `[신규]` 새로 정의 필요 + +### A-1. 재화·자산 `[확정]` — 서버 SOT 필수 (PD 가이드 1) + +| 도메인 | 필드 예시 | 비고 | +|-------|---------|------| +| **재화 인벤토리** | `dic_Item[itemid] = amount` (Dictionary) — Gold·Soul·DailyMissionPoint·WeeklyMissionPoint 등 | 사용 시 항상 서버 응답 필수 | +| **PC(캐릭터) 보유** | `dic_PC[pcid]` — 레벨·경험치·각성 | 재화로 육성되므로 서버 SOT | +| **장비** | `Equipment` (SD_Equipment) + `EquipmentTrade` | 장비 획득·강화·거래 | +| **카드** | `dic_Card[cardid]` (SD_Card) | 카드 수집·업그레이드 | +| **구매 PC ID 세트** | `set_PurchasedPCIDs` (HashSet) | 현금·패키지 구매 대상 → 결제 검증 후 서버 저장 | + +### A-2. 진행도·달성 `[확정]` + +| 도메인 | 필드 예시 | 비고 | +|-------|---------|------| +| **스테이지 진행** | `StageData.dic_stagedata[diff].dic_ChapterData[chapter].list_StageData[].Star` | 난이도·챕터·스테이지별 별 수 | +| **최고 도달 스테이지** | `Get_ClearMaxStageDatas()` 계산값 | 서버 저장 SOT (현재 로컬 계산) | +| **고양이 레벨** | `CatLv`, `CatExp`, `L_CatLvTime` | 메타 성장 지표 | +| **시나리오 시청 기록** | `show_Scenarios` (HashSet) | UX — 서버 저장 권장 (기기 변경 시 재시청 방지) | +| **미션 진행** | `Mission.dic_Mission[type]` — 일일/주간 카운트·보상 수령 여부 | 보상이 재화이므로 서버 SOT | +| **출석** | `RewardAttandanceDay`, `AttandanceDayOfYear` | 보상이 재화이므로 서버 SOT | + +### A-3. 랭킹 `[추가분석]` (PD 가이드 3) + +| 도메인 | 현 구현 | 필요 조치 | +|-------|--------|----------| +| **리더보드 읽기** | `PlayFabProgressionAPI.GetLeaderboard` 사용 중 | 유지 | +| **리더보드 등록** | 호출처 **미확인** (소스 검색 0건) | **인간 개발자가 기존 등록 경로 재구축 or 신규 설계**. PD 가이드 3에 따라 "클라 정보 그대로 저장" 방식 | +| **리더보드 종류** | 현재 함수는 `leaderboard` 문자열 파라미터로 동적 | **리더보드 명세 SOT 필요** — 어떤 랭킹 존재? (스테이지 클리어타임? 최고 도달 스테이지? 고양이 레벨?) 기획팀 정의 필요 | +| **랭킹 보상** | **현재 없음** | PD 가이드 3 — **재화 보상만 서버 처리 가능**. 비재화(칭호·스킨 등) 시 PD 결정 필요 | + +### A-4. 세이브 동기화 `[추가분석]` — **가장 중요한 결정 안건** + +현 구조는 **로컬 1차 + 클라우드 보조** 하이브리드. 이를 어디까지 서버 SOT화 할지가 핵심 결정: + +| 데이터 종류 | 현재 | 권장 방향 | 근거 | +|-----------|------|----------|------| +| 재화·PC·장비·카드 | 로컬 저장 (ObscuredType 난독화) | **서버 SOT** | PD 가이드 1 "재화 사용 항상 서버 응답" | +| 스테이지 진행·별 수 | 로컬 저장 | **서버 SOT** (보상 지급 연결) | PD 가이드 2 보상 연결 | +| 시나리오 시청 기록 | 로컬 저장 | **서버 저장** (기기 변경 복원) | UX | +| UI 옵션·사운드 볼륨 | 로컬 저장 | **로컬 유지** | 서버 저장 불필요 | + +### A-5. 우편 `[확정]` +- 현 구현: `Get_MailList`, `Get_MailReward`, `Get_AllMail` CloudScript 존재 +- 서버 SOT. 재화 보상 우편 → 서버가 수령 처리 + 인벤토리 반영 응답 + +### A-6. 시즌·패스 `[신규]` (메타 시스템 — 추가 분석 필요) +- **현 SOT 부재**. 기획팀 메타 시스템 SOT 착수 후 구체화 +- 예상 필드: 현 시즌 ID, 시즌 진행 XP, 패스 보유 여부, 단계별 보상 수령 상태 +- 시즌 리셋·크로스 시즌 이월은 서버 주도 결정 + +### A-7. 상점 `[신규]` (메타 시스템 — 추가 분석 필요) +- `CatGoodsShop`(`SD_CatGoodsShop`), `EquipmentTrade`(`SD_EquipmentTrade`) 로컬 구조 존재. **일일 리셋 로직 클라에 있음** (`CheckDailyReset`) +- **이슈**: 상점 일일 리셋을 **클라 시간 기반**으로 하면 기기 시간 조작 취약. **서버 시간 기반 리셋 필요** +- 현금 상점(패키지·현금 PC 구매)은 A-1의 `set_PurchasedPCIDs`와 연결 + +### A-8. 소셜·기타 `[확정 일부]` +- **닉네임**: `UpdateUserTitleDisplayName` (PlayFab) — 서버 SOT +- **타 유저 정보 조회**: `Get_OtherUserInfo` (CloudScript) — 서버 SOT +- **계정 탈퇴**: `Del_TitlePlayer` — 서버 SOT + +--- + +## B. 클라/서버 처리 경계 매트릭스 + +> **적용 원칙**: PD 가이드 3종을 각 액션에 기계적 적용. 통신 시점·주체·검증 책임 명시. + +### B-1. 재화 관련 액션 (PD 가이드 1 적용 — 서버 응답 필수) + +| 액션 | 클라 역할 | 서버 역할 | 통신 시점 | 비고 | +|------|---------|----------|----------|------| +| 재화 사용 (예: 카드 업그레이드에 Soul 소비) | 사용 요청 전송 | **잔고 검증·차감·응답** | 요청 즉시 | 서버 거부 시 클라 롤백 | +| 재화 획득 (드롭·보상) | 획득 이벤트 통지 | **지급 검증·적립·응답** | 이벤트 발생 즉시 | 스테이지 완료 번들로 묶어 1회 호출 가능 | +| 재화 잔고 조회 | UI 표시 | SOT 제공 | 로그인·주요 화면 진입 | 서버 잔고가 정답. 클라 캐시 임시 | +| 재화 구매 (현금) | 구매 플로우 실행 | **영수증 검증**(`ValidateGooglePlayPurchase`)·지급 | 영수증 수신 즉시 | InAppInfo 재활성화 필요 | + +### B-2. 미션·퀘스트 (PD 가이드 2 적용 — 클라 판단 + 보상 지급 주체 결정 필요) + +| 액션 | 클라 역할 | 서버 역할 | 통신 시점 | 비고 | +|------|---------|----------|----------|------| +| 미션 조건 체크 (예: 3회 출석) | **클라 자체 판단** | — | — | 클라가 카운트 누적·달성 판정 | +| 미션 완료 통지 | 완료 신호 전송 | 달성 기록 저장 | 완료 즉시 | 단순 기록 | +| **미션 보상 (재화)** | 수령 요청 | **지급·응답** | 요청 즉시 | B-1 "재화 획득"과 동일 | +| **미션 보상 (비재화)** | **클라 단독 지급** (PC·카드·장비) | 지급 결과만 사후 기록 | 지급 후 통지 | **안건 PD-①** 참조 | + +### B-3. 스테이지 진행 (혼합) + +| 액션 | 클라 역할 | 서버 역할 | 통신 시점 | 비고 | +|------|---------|----------|----------|------| +| 인게임 시작 | 선택한 맵 전송 | **세션 등록** (`Save_IngameStart` 현 구현) | 전투 시작 시 | 중복 진입 차단·서버 시간 기준 확보 | +| 인게임 플레이 | 전 로직 클라 주도 | 개입 없음 | — | 실시간 통신 없음 | +| 스테이지 완료 | 결과 전송 (별 수·드롭 아이템·클리어 시간) | **재화 지급 + 진행도 갱신** | 완료 즉시 | `Save_StageResult` 복원·신규 설계 필요 | +| 별 보상 수령 (비재화) | 클라 단독 | — | — | B-2 비재화 보상과 동일 | + +### B-4. 랭킹 (PD 가이드 3 적용) + +| 액션 | 클라 역할 | 서버 역할 | 통신 시점 | 비고 | +|------|---------|----------|----------|------| +| 랭킹 등록 | 점수·메타정보 계산·전송 | **검증 없이 그대로 저장** | 스테이지 완료·시즌 갱신 시 | PD 가이드 3 | +| 내 랭킹 조회 | 요청 | `GetLeaderboardAroundEntity` 응답 | 랭킹 UI 진입 | 현 구현 있음 | +| 상위 랭킹 조회 | 요청 | `GetLeaderboard` 응답 | 랭킹 UI 진입 | 현 구현 있음 | +| **랭킹 보상 (재화)** | 수령 요청 | **지급·응답** | 시즌 종료 시 | B-1과 동일 | +| **랭킹 보상 (비재화)** | ? | **서버 처리 불가** | ? | **안건 PD-②** 참조 | + +### B-5. 세이브·메타 + +| 액션 | 클라 역할 | 서버 역할 | 통신 시점 | 비고 | +|------|---------|----------|----------|------| +| 로그인·최초 데이터 로드 | 요청 | 전체 데이터 응답 | 앱 실행 시 | 현 `Get_UserInfo` 구조 확장 | +| 주기 세이브 | 변경분 전송 | 저장 | 주요 결정 직후 (결제·전투 완료·재화 변동) | 빈도 최소화, 이벤트 기반 | +| 기기 변경 복원 | 로그인 ID 전송 | 동일 계정 데이터 응답 | 신규 기기 로그인 시 | 계정 연결 정책 별도 필요 | +| 일일/주간 리셋 | 리셋 UI 표시 | **서버 시간 기반 판정·리셋 응답** | 로그인·일일 최초 접속 | 현재 클라 `DayOfYear` 비교는 취약 | +| 시나리오 시청 기록 | 시청 후 통지 | 기록 저장 | 시청 직후 | 기기 변경 시 복원 | + +### B-6. 우편·소셜 + +| 액션 | 클라 역할 | 서버 역할 | 통신 시점 | 비고 | +|------|---------|----------|----------|------| +| 우편 조회 | 요청 | 목록 응답 | 우편함 진입 | 현 구현 있음 | +| 우편 수령 (재화 포함) | 수령 요청 | **지급·인벤토리 반영·응답** | 요청 즉시 | 현 구현 있음 | +| 닉네임 변경 | 입력값 전송 | 중복 검사·저장 | 변경 시 | 현 구현 있음 | + +--- + +## C. 의사결정 필요 항목 (PD님 안건) + +### PD-① 미션·스테이지·업적 보상 지급 주체 (PD 가이드 2 연장) + +**안건**: 비재화 보상(장비·카드·캐릭터 등)의 지급 주체를 어떻게 할 것인가? + +**현 실측 제약**: +- `dic_PC`·`dic_Card`·`Equipment` 등 비재화 자산도 **현재 ServerData 로컬 SOT에 포함** +- PlayFab CloudScript는 재화 인벤토리 중심, 장비·카드 지급 로직 미구현 +- PD 전제: "재화 아닌 보상은 서버 처리 어려움" + +**안 2종**: +1. **A안 — 클라 단독 지급 + 사후 서버 동기화** (PD 전제 그대로) + - 가) 클라가 보상 지급 + ServerData 로컬 갱신 + - 나) 다음 주기 세이브에서 서버 반영 + - 다) **리스크**: 지급 직후 앱 강제 종료 시 미반영 — 재지급/유실 판정 정책 필요 +2. **B안 — 비재화 자산도 서버 SOT 전환** (PD 전제 초과) + - 가) 장비·카드·PC 지급 API 신설 (`Grant_Equipment`·`Grant_Card` CloudScript) + - 나) 서버가 유니크 인벤토리 관리 + - 다) **비용**: 개발 공수 + PlayFab 인벤토리 모델 재설계 + +**개발팀장 의견**: A안 기본 + 지급 멱등성 보장용 클라 임시 저널(로컬 큐)로 유실 방지. PD 전제 정합. + +--- + +### PD-② 비재화 랭킹 보상 처리 방안 (PD 가이드 3 연장) + +**안건**: 랭킹 시즌 종료 시 비재화 보상(칭호·스킨·특수 장비)을 어떻게 지급할 것인가? + +**현 실측 제약**: +- 현재 랭킹 등록 경로 미확인 + 랭킹 보상 시스템 자체 미구현 +- PD 전제: "재화 아닌 랭킹 보상은 서버 처리 불가" + +**안 2종**: +1. **A안 — 재화 우편 보상만 운영** (PD 전제 그대로) + - 가) 시즌 종료 시 서버가 순위별 재화 우편 발송 + - 나) 칭호·스킨 등 비재화 보상은 설계에서 제외 + - 다) **제약**: 랭킹 동기 부여 수단 축소 +2. **B안 — 클라 단독 지급 + 서버는 "대상자 여부" 플래그만 관리** + - 가) 서버: 시즌 종료 시 순위 확정 → 대상자 플래그 저장 (예: `SeasonReward_S03_Tier1: true`) + - 나) 클라: 플래그 확인 후 로컬에서 비재화 보상 해금 + - 다) **리스크**: 플래그 조작 시 부정 해금 — AntiCheat로 일부 방어 가능 + +**개발팀장 의견**: B안 권장. 서버 처리 범위를 "대상자 판정"으로 한정하면 PD 전제("재화 아닌 서버 처리 불가") 정신 유지 + 랭킹 유인 확보. + +--- + +### PD-③ 서버 스택 유지 여부 (PlayFab vs 자체 서버) + +**안건**: PlayFab을 계속 사용할 것인가, 자체 서버(Node.js + MongoDB 등)로 전환할 것인가? + +**현 상태**: PlayFab 광범위 의존 (인증·CloudScript·Leaderboard·Profiles·Mail·IAP 검증). `PD 지시 #2`에서 서버 Critical 보안 3건 보류 중. + +**안 3종**: +1. **A안 — PlayFab 유지 + CloudScript 확장** (현재 기조 유지) +2. **B안 — PlayFab 인증·Leaderboard만 유지 + 게임 로직은 자체 서버** +3. **C안 — 완전 자체 서버 전환** + +**개발팀장 의견**: 본 안건은 **인간 서버 개발자 배정 후 해당 개발자의 기술 스택 선호·경험과 함께 결정 권장**. 본 초안은 A안(PlayFab 유지) 전제로 작성되었으나, 개발자 배정 시 재검토 필요. + +--- + +### PD-④ 세이브 SOT 전환 범위 + +**안건**: 현재 **로컬 1차 + 클라우드 보조** 하이브리드를 **서버 1차 SOT**로 전환할 것인가? + +**안 2종**: +1. **A안 — 현 하이브리드 유지** — 최소 변경. 재화·결제만 서버 검증 +2. **B안 — 서버 1차 SOT 전환** — 로컬은 오프라인 캐시로만. 모든 변경 서버 저장 + +**개발팀장 의견**: **B안이 원칙적으로 옳으나 수상한잡화점 현 시점에는 A안 유지 권장**. 이유: 서버 Critical 3건 보류 중 + 게임 오프라인 플레이 비중 고려. 차기 프로젝트는 B안 기반 설계 (헌법 제1원칙 목표 2 — 코어 프레임워크에 반영 권장). + +--- + +### PD-⑤ 일일/주간 리셋 시간 기준 + +**안건**: 현재 `DayOfYear` 클라 시간 비교 방식을 서버 시간 기반으로 전환할 것인가? + +**현 이슈**: 기기 시간 조작으로 일일 출석·일일 상점 어뷰즈 가능. + +**개발팀장 의견**: **서버 시간 기준 전환 필수**. PD 재검토 불요 수준의 보안 기본. 서버 Critical 보안 3건 재개 시 포함 권장. + +--- + +## D. 인간 서버 개발자 착수 시 선행 작업 (C30·P13·P18 정합) + +1. **선행 Read 패키지** (서버팀장 사전 보고 서버 재개 예비 C-1 적용) + - 가) 본 문서 + - 나) `공유/PD_지시_트래킹/개발팀_PD_지시_로그.md` #2 (서버 Critical 3건) + - 다) 현 `ServerClass.cs`·`ServerInfo.cs` 전문 + - 라) `memory/org/feedback_*.md` 중 서버·보안 관련 + +2. **환경 셋업** + - 가) PlayFab 계정·TitleId 확인 (현 코드 `Server_Live_ID`·`Server_Dev_ID` 상수 주석 처리 상태) + - 나) CloudScript 현 코드 dump 및 버전 관리 편입 + - 다) Unity 클라이언트 개발 환경 (수상한잡화점 브랜치 pull) + +3. **설계 문서화 의무 (P18)** + - 가) 본 초안의 PD 결정 후 **서버 아키텍처 공식 설계 문서** 작성 (`프로젝트/수상한잡화점/서버/01_서버_아키텍처_v1.md` 신규) + - 나) API 계약서 작성 (`02_API_계약서_v1.md`) — 엔드포인트·파라미터·응답·에러 코드 + - 다) CloudScript 함수 명세 (`03_CloudScript_명세_v1.md`) + +4. **P13 공용 인터페이스 변경 사전 공유** + - 가) 클라이언트팀(현 개발팀장)과 데이터 포맷 협의 + - 나) 기획팀과 재화·미션·랭킹 밸런스 협의 + +--- + +## E. 산출물 요약 + +| 분류 | 건수 | 긴급도 | +|------|------|--------| +| A. 데이터 카테고리 | 8개 도메인 (재화·진행도·랭킹·세이브·우편·시즌·상점·소셜) | 정리 완료 | +| B. 클라/서버 매트릭스 | 6개 액션군 × 약 25건 | 정리 완료 | +| C. PD 결정 안건 | 5건 (보상 지급 주체·비재화 랭킹·서버 스택·SOT 범위·리셋 시간) | PD님 결정 대기 | +| D. 선행 작업 | 4단 | 인간 개발자 배정 시 적용 | + +--- + +## F. 기각안 (P24·P27 개정 — 결정·설계 엔트리 필수) + +본 초안이 채택하지 않은 대안들: + +1. **"서버 처리를 최소화하고 전부 클라 주도로" 안** — PD 가이드 1(재화 서버 필수)과 충돌. 어뷰즈 방어 근거 소멸. 기각 +2. **"모든 비재화 자산을 서버 SOT로 전환" 안** — PD 전제("재화 아닌 서버 처리 어려움") 초과. 개발 공수 큼. PD-① B안으로만 옵션 제시하고 초안 본문에서는 비채택 +3. **"PlayFab 전면 폐기 + 자체 서버 전면 신규" 안** — 서버 Critical 3건 보류 상태에서 범위 확대는 리스크. PD-③에 옵션으로만 제시하고 본 초안은 PlayFab 유지 전제 +4. **"인간 개발자 배정 전 세부 API 스펙까지 확정" 안** — C9(완성도 우선) 정신이나, **배정된 개발자의 기술 선호·경험 반영이 없는 API 스펙은 재작업 리스크**. 본 초안은 원칙·경계·안건까지 정리하고 세부 스펙은 개발자 배정 후 + +--- + +## G. PM 처리 권장 + +1. **PD님 보고**: 본 초안 + PD-①~⑤ 5건 안건 (C29-3 목표 3 정신 — 결정 요청 최소화된 형태) +2. **PD 결정 수령 시 후속**: 개발팀 PD 지시 로그 #2 재개 트리거 조건 명확화 (서버 Critical 3건 + 본 초안 PD 결정 연계) +3. **인간 개발자 배정 시**: 본 초안 + D-1 선행 Read 패키지 동봉하여 온보딩 +4. **차기 프로젝트 코어 프레임워크 반영**: PD-④ B안(서버 1차 SOT), PD-⑤(서버 시간 기준)는 헌법 제1원칙 목표 2 — 차기 프로젝트 프레임워크 기본 탑재 권장 + +--- + +**서명**: 개발팀장 (서버팀장·클라이언트팀장 Agent 호출 환경 제약으로 단독 분석, C23 정직성 준수) +**검증 권장**: dev-auditor 모드 A 1회 + PM 교차 검토 후 PD 상신 diff --git a/공유/소통/개발팀→PM/2026-04-17_Unity_MCP_시뮬레이션_기술검토_개발팀.md b/공유/소통/완료/2026-04-17_Unity_MCP_시뮬레이션_기술검토_개발팀.md similarity index 100% rename from 공유/소통/개발팀→PM/2026-04-17_Unity_MCP_시뮬레이션_기술검토_개발팀.md rename to 공유/소통/완료/2026-04-17_Unity_MCP_시뮬레이션_기술검토_개발팀.md diff --git a/공유/소통/기획팀→PM/2026-04-17_Unity_MCP_시뮬레이션_기획검토_기획팀.md b/공유/소통/완료/2026-04-17_Unity_MCP_시뮬레이션_기획검토_기획팀.md similarity index 100% rename from 공유/소통/기획팀→PM/2026-04-17_Unity_MCP_시뮬레이션_기획검토_기획팀.md rename to 공유/소통/완료/2026-04-17_Unity_MCP_시뮬레이션_기획검토_기획팀.md diff --git a/공유/소통/완료/2026-04-17_감사보고_재량작업_일괄완료.md b/공유/소통/완료/2026-04-17_감사보고_재량작업_일괄완료.md new file mode 100644 index 0000000..694160f --- /dev/null +++ b/공유/소통/완료/2026-04-17_감사보고_재량작업_일괄완료.md @@ -0,0 +1,74 @@ +--- +from: plan-auditor +to: PM +type: 감사보고 +subject: 기획팀장 재량작업 3건(#33·#34·#35) + 기획 결정 2건(#30·#31) 모드 B 주기 감사 +priority: MID +status: 완료 +created: 2026-04-17 +related_pd_지시: 기획팀 #30·#31·#33·#34·#35 +related_commit: 06a7bb8 +--- + +# plan-auditor 모드 B 감사 보고 + +## 1. 감사 범위 +- PD 지시 #33 REQ 템플릿, #34 에이전트 6종 기록 의무, #35 밸런싱 md 4종 변경 이력 +- 연관 #30(맥락 오류 점검)·#31(P24 기각안 필수화) 정합성 교차 +- 조직운영 대화로그·수상한잡화점 대화로그·소통 채널·PD 지시 로그 실측 + +## 2. 실측 근거 (tool_use 입증) +| 항목 | 결과 | +|---|---| +| `REQ-템플릿_밸런스수치.md` | 존재, 5923 bytes, YAML 프론트매터 7필드 이상 포함 | +| 밸런싱 md 4종 "변경 이력 (P16 산출물 추적성)" 섹션 | 4종 전부 존재 (line 313·168·587·378) | +| 에이전트 6종 (balance/content/level/narrative/system/ux) | 6종 전부 "기록 의무"·"plan-auditor"·"기각안 필드 필수" 키워드 각 4회 매칭 (일관 구조) | +| 기획팀 PD 지시 로그 #33·#34·#35 | 완료 아카이브 이동 완료, 산출물 경로 실존 | +| 조직운영 대화로그 2026-04-17 기획팀장 엔트리 | 기각안 3건 명시(P24 신규 필수화 요구 충족) | +| 수상한잡화점 2026-04-17 대화로그 #33·#34·#35 엔트리 | **부재** (조직운영 로그에만 기록) | + +## 3. 분류 결과 + +### Critical — 없음 + +### Major — 1건 +**M1. 수상한잡화점 대화로그 누락 (P24·P27-4 SOT 경계 혼선)** +- 현상: #33·#34·#35는 수상한잡화점 프로젝트 산출물(REQ·밸런싱 md) 영향이 직접적이나 `공유/대화로그/수상한잡화점/2026-04-17.md`에 기획팀장 3건 일괄 엔트리가 없음. 조직운영 로그에만 기록. +- 영향: 프로젝트별 대화로그로 차기 프로젝트 참고 자료를 추출할 때 수상한잡화점 파일에 기획팀 변경 맥락 누락 → 목표 2 원칙 B 인사이트 소실 위험. +- 정정 방안: `공유/대화로그/수상한잡화점/2026-04-17.md`에 1건 append (요지·이유·산출물·상태·기각안 3종 요약, 조직운영 엔트리 교차 참조). + +### Minor — 1건 +**m1. REQ 템플릿 "관련PD지시" 필드는 선택이나, 템플릿 본문에 "기각안 필드 예시 값" 미제공** +- P24 개정 직후 신설이므로 차기 REQ 작성자가 공란 혼선 위험. +- 정정 방안: 템플릿 "기각안" 섹션에 예시 1행 추가(선택적 권고). + +### Improvement — 2건 +**I1. 밸런싱 md 변경 이력 "기각안" 컬럼 부재** +- 현재 포맷: 일시/변경자/변경 필드/이전값→이후값/재미 근거/관련 PD 지시#. P24 필수화가 결정·설계 엔트리 범위로 밸런스 수치 변경도 해당. 컬럼 1개 추가 권고. + +**I2. 기획팀장 2026-04-17_업무공유체계_점검_기획팀.md 안건 1이 P24 개정으로 종결되었는데, 소통 파일 status가 "완료"로 이동·표기되었는지 미확인** +- PM이 `공유/소통/완료/` 이동 운영 원칙(C29-4) 확인 필요. + +## 4. 규칙 준수 스코어 +| 규칙 | 준수 | 비고 | +|---|---|---| +| C5 정직성 | O | 기각안 3종 명시(기획팀장 엔트리) | +| C6 데이터 보호 | O (해당 없음 — 백업 대상 xlsm 변경 없음) | — | +| C7 재미 우선 | O | 변경 이력 "재미 근거" 필드 표준화 | +| C13·C27·C29-4 | O | PD 지시 로그 활성→아카이브 이동 완료 | +| P16 추적성 | O | 4종 테이블 신설 | +| P17 배타 배치 | 해당 없음 | — | +| P18 설계 문서화 | O | 참조된 문서 전부 실존 | +| **P24 기각안 필수** | **부분 O** | 조직운영 엔트리 충족, 수상한잡화점 엔트리 부재(M1) | +| P27-2 호출 프롬프트 3요소 | 해당 없음 (기획팀장이 Agent 재호출 불가) | — | +| P27-4 SOT 경계 | 부분 O | 프로젝트 축 누락(M1) | + +## 5. PM 요청 조치 +1. M1 즉시 시정 — `공유/대화로그/수상한잡화점/2026-04-17.md` 기획팀장 3건 일괄 엔트리 append (PM 재량 처리 권고) +2. I1 검토 후 밸런싱 md 4종 변경 이력 포맷에 "기각안" 컬럼 추가 여부 결정 (기획팀장에게 차기 위임) +3. I2 `공유/소통/` 완료 이동 운영 일괄 점검 (pm-auditor 감사 C1과 동일 패턴 — 중복 Major 방지) + +## 6. 노하우 축적 (차기 프로젝트 참고) +- **기획 결정의 기각안은 조직운영 로그에 집중되는 경향** — 수상한잡화점 프로젝트 로그에도 동시 기록이 필요 (P27-4 양방향 SOT 유지) +- **밸런싱 md 변경 이력 테이블은 인라인 기록이 누락 방지에 효과적** — 기획팀장 기각안 3(인라인 유지) 타당성 실증 +- **전문 에이전트 6종의 공통 지침은 SKILL.md 참조형 + 영역 특화 인라인 혼합**이 최적(C14-4 vs 체감 준수율 균형) — 기획팀장 기각안 2 타당성 실증 diff --git a/공유/소통/pm-auditor→PM/2026-04-17_감사보고_팀기록체계_전수점검.md b/공유/소통/완료/2026-04-17_감사보고_팀기록체계_전수점검.md similarity index 100% rename from 공유/소통/pm-auditor→PM/2026-04-17_감사보고_팀기록체계_전수점검.md rename to 공유/소통/완료/2026-04-17_감사보고_팀기록체계_전수점검.md diff --git a/공유/소통/개발팀→PM/2026-04-17_업무공유체계_점검_개발팀.md b/공유/소통/완료/2026-04-17_업무공유체계_점검_개발팀.md similarity index 100% rename from 공유/소통/개발팀→PM/2026-04-17_업무공유체계_점검_개발팀.md rename to 공유/소통/완료/2026-04-17_업무공유체계_점검_개발팀.md diff --git a/공유/소통/기획팀→PM/2026-04-17_업무공유체계_점검_기획팀.md b/공유/소통/완료/2026-04-17_업무공유체계_점검_기획팀.md similarity index 100% rename from 공유/소통/기획팀→PM/2026-04-17_업무공유체계_점검_기획팀.md rename to 공유/소통/완료/2026-04-17_업무공유체계_점검_기획팀.md diff --git a/공유/소통/개발팀→PM/2026-04-17_업무공유체계_점검_서버팀.md b/공유/소통/완료/2026-04-17_업무공유체계_점검_서버팀.md similarity index 100% rename from 공유/소통/개발팀→PM/2026-04-17_업무공유체계_점검_서버팀.md rename to 공유/소통/완료/2026-04-17_업무공유체계_점검_서버팀.md diff --git a/공유/소통/개발팀→PM/2026-04-17_업무공유체계_점검_클라이언트팀.md b/공유/소통/완료/2026-04-17_업무공유체계_점검_클라이언트팀.md similarity index 100% rename from 공유/소통/개발팀→PM/2026-04-17_업무공유체계_점검_클라이언트팀.md rename to 공유/소통/완료/2026-04-17_업무공유체계_점검_클라이언트팀.md diff --git a/공유/소통/개발팀→PM/2026-04-17_업무현황_개발팀.md b/공유/소통/완료/2026-04-17_업무현황_개발팀.md similarity index 100% rename from 공유/소통/개발팀→PM/2026-04-17_업무현황_개발팀.md rename to 공유/소통/완료/2026-04-17_업무현황_개발팀.md diff --git a/공유/소통/기획팀→PM/2026-04-17_업무현황_기획팀.md b/공유/소통/완료/2026-04-17_업무현황_기획팀.md similarity index 100% rename from 공유/소통/기획팀→PM/2026-04-17_업무현황_기획팀.md rename to 공유/소통/완료/2026-04-17_업무현황_기획팀.md diff --git a/코어코드/NerdNavis.Framework/CHANGELOG.md b/코어코드/NerdNavis.Framework/CHANGELOG.md index ccd4abe..9060610 100644 --- a/코어코드/NerdNavis.Framework/CHANGELOG.md +++ b/코어코드/NerdNavis.Framework/CHANGELOG.md @@ -8,6 +8,19 @@ ### Added - 패키지 스켈레톤 (폴더 구조, asmdef, package.json) +- Tier 1 기반 Core 4종: `Log`, `CoroutineRunner`, `MonoSingleton`, `ServiceLocator` (+ 테스트 28건) +- Tier 1 Attribute 3종 (2026-04-17) + - `NerdNavis.Core.Attribute.ReadOnlyAttribute` — 인스펙터 읽기 전용 + - `NerdNavis.Core.Attribute.ShowIfAttribute` — 조건부 인스펙터 노출 + - `NerdNavis.Core.Attribute.ArrayTitleAttribute` — 배열 요소 라벨 커스터마이즈 +- Tier 1 Util 6종 (2026-04-17) + - `NerdNavis.Core.Util.EnumToInt` — 박싱-프리 enum ↔ int 변환 + - `NerdNavis.Core.Util.EnumEx` — enum 메타데이터·파싱 유틸 (캐시 기반) + - `NerdNavis.Core.Util.FormatEx` — 수치 축약·시간·확률·바이트 포맷 + - `NerdNavis.Core.Util.MathEx` — Clamp·Remap·SmoothTowards·GCD/LCM 등 + - `NerdNavis.Core.Util.KeyMaker` — `':'` 구분자 합성 키 생성 + - `NerdNavis.Core.Util.ValidationEx` — 인자·상태 가드(NotNull·InRange·Positive 등) +- 위 9종 단위 테스트 추가 (NUnit, Tests/Runtime/Core/Util + Attribute) ## [0.1.0] - TBD diff --git a/코어코드/NerdNavis.Framework/Runtime/Core/Attribute/ArrayTitleAttribute.cs b/코어코드/NerdNavis.Framework/Runtime/Core/Attribute/ArrayTitleAttribute.cs new file mode 100644 index 0000000..d0900d9 --- /dev/null +++ b/코어코드/NerdNavis.Framework/Runtime/Core/Attribute/ArrayTitleAttribute.cs @@ -0,0 +1,54 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework +// ArrayTitleAttribute.cs — 배열 요소별 인스펙터 라벨 커스터마이즈 속성 +// --------------------------------------------------------------------------- +using System; +using UnityEngine; + +namespace NerdNavis.Core.Attribute +{ + /// + /// 배열·리스트의 각 요소를 인스펙터에서 표시할 때 + /// "Element N" 대신 지정 멤버의 값을 라벨로 사용하도록 지시한다. + /// + /// + /// 설계 문서 §5 Tier 1. 데이터 테이블·카드 리스트 등 요소 식별이 잦은 컬렉션을 + /// 인스펙터에서 빠르게 탐색할 수 있도록 한다. + /// 동작: + /// + /// 요소 타입 내부의 필드 또는 프로퍼티를 읽어 + /// 결과를 라벨로 사용. + /// 해당 멤버를 찾지 못하면 기본 "Element N" 라벨로 폴백. + /// 가 지정된 경우 접두어로 덧붙인다(예: "Card: Fireball"). + /// + /// 실제 그리기는 Editor 스크립트 ArrayTitleDrawer에서 담당한다. + /// 사용 예: + /// + /// [SerializeField, ArrayTitle(nameof(CardEntry.DisplayName), "Card")] + /// private List<CardEntry> _cards; + /// + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)] + public sealed class ArrayTitleAttribute : PropertyAttribute + { + /// 요소 내부에서 라벨로 사용할 멤버 이름. + public string MemberName { get; } + + /// 라벨 접두어(옵션). 빈 문자열이면 접두어 없이 멤버 값만 표시. + public string Prefix { get; } + + /// 기본 생성자. 접두어 없이 멤버 값만 라벨로 사용한다. + public ArrayTitleAttribute(string memberName) + { + MemberName = memberName; + Prefix = string.Empty; + } + + /// 접두어와 함께 라벨을 구성한다. 표시 형식: "{Prefix}: {MemberValue}". + public ArrayTitleAttribute(string memberName, string prefix) + { + MemberName = memberName; + Prefix = prefix ?? string.Empty; + } + } +} diff --git a/코어코드/NerdNavis.Framework/Runtime/Core/Attribute/ReadOnlyAttribute.cs b/코어코드/NerdNavis.Framework/Runtime/Core/Attribute/ReadOnlyAttribute.cs new file mode 100644 index 0000000..857a22f --- /dev/null +++ b/코어코드/NerdNavis.Framework/Runtime/Core/Attribute/ReadOnlyAttribute.cs @@ -0,0 +1,27 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework +// ReadOnlyAttribute.cs — 인스펙터 읽기 전용 표시 속성 +// --------------------------------------------------------------------------- +using System; +using UnityEngine; + +namespace NerdNavis.Core.Attribute +{ + /// + /// 필드를 Unity 인스펙터에서 읽기 전용으로 표시하도록 지시한다. + /// + /// + /// 설계 문서 §5 Tier 1. 기존 NerdNavisCore DG.Core.Attribute.ReadOnlyAttribute + /// 구조를 승계하되 네임스페이스만 로 이전했다. + /// 실제 그리기(GUI.enabled=false)는 Editor 스크립트 ReadOnlyDrawer에서 수행한다. + /// 사용 예: + /// + /// [SerializeField, ReadOnly] private int _runtimeOnly; + /// + /// 주의: 런타임 직렬화 동작에는 영향을 주지 않으며 인스펙터 편집 차단만 담당한다. + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)] + public sealed class ReadOnlyAttribute : PropertyAttribute + { + } +} diff --git a/코어코드/NerdNavis.Framework/Runtime/Core/Attribute/ShowIfAttribute.cs b/코어코드/NerdNavis.Framework/Runtime/Core/Attribute/ShowIfAttribute.cs new file mode 100644 index 0000000..3e32575 --- /dev/null +++ b/코어코드/NerdNavis.Framework/Runtime/Core/Attribute/ShowIfAttribute.cs @@ -0,0 +1,58 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework +// ShowIfAttribute.cs — 조건부 인스펙터 노출 속성 +// --------------------------------------------------------------------------- +using System; +using UnityEngine; + +namespace NerdNavis.Core.Attribute +{ + /// + /// 동일 오브젝트의 다른 필드·프로퍼티 값이 와 일치할 때만 + /// 인스펙터에 필드를 표시한다. + /// + /// + /// 설계 문서 §5 Tier 1. 기존 코어의 조건부 노출 기능을 범용화했다. + /// 비교 규칙: + /// + /// null이면 대상 멤버의 truthy 여부로 판정 + /// (bool은 자기자신, 숫자는 0이 아닐 때, 참조는 non-null 일 때 true). + /// 값 비교는 로 수행한다. + /// 대상 멤버가 없거나 접근 불가면 필드는 기본 노출된다(Editor 오류 대신 안전한 기본값). + /// + /// 실제 그리기는 Editor 스크립트 ShowIfDrawer에서 담당하며, 런타임 동작에는 영향을 주지 않는다. + /// 사용 예: + /// + /// [SerializeField] private bool _useCustomRange; + /// [SerializeField, ShowIf(nameof(_useCustomRange))] private Vector2 _range; + /// + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)] + public sealed class ShowIfAttribute : PropertyAttribute + { + /// 감시할 멤버(필드·프로퍼티) 이름. + public string MemberName { get; } + + /// 비교 대상 값. null이면 truthy 비교를 수행한다. + public object ExpectedValue { get; } + + /// 조건 불일치 시 인스펙터에서 완전히 숨길지 여부. false면 비활성화만. + public bool HideInInspector { get; } + + /// 단순 truthy 조건을 설정한다. + public ShowIfAttribute(string memberName, bool hideInInspector = true) + { + MemberName = memberName; + ExpectedValue = null; + HideInInspector = hideInInspector; + } + + /// 특정 값과 일치할 때만 노출한다. + public ShowIfAttribute(string memberName, object expectedValue, bool hideInInspector = true) + { + MemberName = memberName; + ExpectedValue = expectedValue; + HideInInspector = hideInInspector; + } + } +} diff --git a/코어코드/NerdNavis.Framework/Runtime/Core/Util/EnumEx.cs b/코어코드/NerdNavis.Framework/Runtime/Core/Util/EnumEx.cs new file mode 100644 index 0000000..7349534 --- /dev/null +++ b/코어코드/NerdNavis.Framework/Runtime/Core/Util/EnumEx.cs @@ -0,0 +1,93 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework +// EnumEx.cs — enum 메타데이터·열거·변환 확장 유틸리티 +// --------------------------------------------------------------------------- +using System; +using System.Collections.Generic; + +namespace NerdNavis.Core.Util +{ + /// + /// enum 메타데이터(정의된 값 목록·최소/최대·TryParse 등)를 다루는 확장 유틸리티. + /// + /// + /// 설계 문서 §5 Tier 1. 는 매 호출 시 배열을 새로 할당하므로, + /// 본 유틸은 타입별로 값 배열을 캐시하여 재사용한다(C11 자원 효율성). + /// 제공 기능: + /// + /// : 정의된 값의 읽기 전용 배열(캐시된 인스턴스). + /// : 정의된 이름의 읽기 전용 배열(캐시). + /// : 정의된 값인지 박싱 없이 확인. + /// : 대소문자 무시 파싱. + /// : 정의된 값 개수. + /// + /// + public static class EnumEx + { + private static class Cache where TEnum : struct, Enum + { + public static readonly TEnum[] Values; + public static readonly string[] Names; + public static readonly HashSet DefinedInts; + + static Cache() + { + Values = (TEnum[])Enum.GetValues(typeof(TEnum)); + Names = Enum.GetNames(typeof(TEnum)); + DefinedInts = new HashSet(); + for (int i = 0; i < Values.Length; i++) + { + DefinedInts.Add(EnumToInt.Convert(Values[i])); + } + } + } + + /// 정의된 enum 값을 배열로 반환한다(캐시된 인스턴스, 수정 금지). + public static TEnum[] GetValues() where TEnum : struct, Enum + { + return Cache.Values; + } + + /// 정의된 enum 이름을 배열로 반환한다(캐시된 인스턴스, 수정 금지). + public static string[] GetNames() where TEnum : struct, Enum + { + return Cache.Names; + } + + /// 정의된 enum 값의 개수를 반환한다. + public static int Count() where TEnum : struct, Enum + { + return Cache.Values.Length; + } + + /// 값이 enum에 정의되어 있는지 확인한다(박싱 없음). + /// 는 박싱·선형 탐색을 하지만 + /// 본 메서드는 조회로 O(1) 처리한다. + public static bool IsDefined(TEnum value) where TEnum : struct, Enum + { + return Cache.DefinedInts.Contains(EnumToInt.Convert(value)); + } + + /// 문자열을 enum으로 파싱한다. + /// enum 값 이름. + /// 대소문자 무시 여부(기본 true). + /// 파싱 성공 시 값, 실패 시 default. + /// 성공 시 true. + public static bool TryParse(string name, bool ignoreCase, out TEnum result) + where TEnum : struct, Enum + { + if (string.IsNullOrEmpty(name)) + { + result = default; + return false; + } + return Enum.TryParse(name, ignoreCase, out result); + } + + /// 대소문자를 무시하여 파싱한다(편의 오버로드). + public static bool TryParse(string name, out TEnum result) where TEnum : struct, Enum + { + return TryParse(name, true, out result); + } + } +} diff --git a/코어코드/NerdNavis.Framework/Runtime/Core/Util/EnumToInt.cs b/코어코드/NerdNavis.Framework/Runtime/Core/Util/EnumToInt.cs new file mode 100644 index 0000000..998e0a5 --- /dev/null +++ b/코어코드/NerdNavis.Framework/Runtime/Core/Util/EnumToInt.cs @@ -0,0 +1,54 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework +// EnumToInt.cs — boxing-free enum ↔ int 변환 유틸리티 +// --------------------------------------------------------------------------- +using System; +using System.Runtime.CompilerServices; + +namespace NerdNavis.Core.Util +{ + /// + /// enum 값을 로 변환할 때 발생하는 박싱을 회피하는 유틸리티. + /// + /// + /// 설계 문서 §5 Tier 1 (C11 자원 효율성). Convert.ToInt32((object)e) 또는 + /// (int)(object)e 는 박싱을 유발하지만, Unsafe.As<TEnum, int> 를 이용하면 + /// JIT 단계에서 직접 변환되어 힙 할당이 전혀 발생하지 않는다. + /// 사용 전제: + /// + /// 대상 enum의 기저 타입이 여야 한다(·는 미지원). + /// hot-path(매 프레임 수백~수천 회 호출)에서 사용해 효과가 크다. + /// Dictionary·HashSet의 key로 enum을 쓸 때는 별도 IEqualityComparer로 감싸서 박싱을 함께 회피한다. + /// + /// 사용 예: + /// + /// int id = EnumToInt.Convert(CardType.Attack); + /// CardType t = EnumToInt.FromInt<CardType>(id); + /// + /// + public static class EnumToInt + { + /// enum을 int로 변환한다(박싱 없음). + /// 기저 타입 인 enum. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Convert(TEnum value) where TEnum : struct, Enum + { + return Unsafe.As(ref value); + } + + /// int 값을 enum으로 변환한다(박싱 없음). + /// 기저 타입 인 enum. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TEnum FromInt(int value) where TEnum : struct, Enum + { + return Unsafe.As(ref value); + } + + /// 두 enum 값의 동등성을 박싱 없이 비교한다. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Equals(TEnum a, TEnum b) where TEnum : struct, Enum + { + return Unsafe.As(ref a) == Unsafe.As(ref b); + } + } +} diff --git a/코어코드/NerdNavis.Framework/Runtime/Core/Util/FormatEx.cs b/코어코드/NerdNavis.Framework/Runtime/Core/Util/FormatEx.cs new file mode 100644 index 0000000..b77ad7f --- /dev/null +++ b/코어코드/NerdNavis.Framework/Runtime/Core/Util/FormatEx.cs @@ -0,0 +1,96 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework +// FormatEx.cs — 게임 빈출 포맷(수치 축약·시간·확률) 유틸리티 +// --------------------------------------------------------------------------- +using System; +using System.Globalization; +using System.Text; + +namespace NerdNavis.Core.Util +{ + /// + /// 게임에서 자주 쓰이는 수치·시간·확률 포맷 변환을 모은 정적 유틸리티. + /// + /// + /// 설계 문서 §5 Tier 1. 로컬라이제이션 컨텍스트가 없는 순수 수치 포맷만 담당한다. + /// 언어별 라벨(예: "골드")은 NerdNavis.Localization에서 별도로 처리한다. + /// Invariant 문화권을 사용해 디바이스 로케일과 무관한 일관 출력 보장(C11 코드 범용성). + /// + public static class FormatEx + { + private static readonly string[] s_AbbrevSuffixes = { "", "K", "M", "B", "T" }; + + /// 큰 수를 K/M/B/T 단위로 축약한다. + /// 원본 정수. + /// 소수 자리수(기본 1). + /// 예: 12345 → "12.3K", 1_500_000 → "1.5M". + public static string AbbreviateNumber(long value, int decimals = 1) + { + if (value == 0) return "0"; + + long abs = value < 0 ? -value : value; + int tier = 0; + double scaled = abs; + while (scaled >= 1000.0 && tier < s_AbbrevSuffixes.Length - 1) + { + scaled /= 1000.0; + tier++; + } + + string sign = value < 0 ? "-" : string.Empty; + if (tier == 0) + { + return sign + abs.ToString(CultureInfo.InvariantCulture); + } + + string format = decimals > 0 ? "F" + decimals : "F0"; + return sign + scaled.ToString(format, CultureInfo.InvariantCulture) + s_AbbrevSuffixes[tier]; + } + + /// 초 단위 시간을 "mm:ss" 또는 "hh:mm:ss"로 포맷한다. + public static string FormatTimeSpan(float seconds) + { + if (seconds < 0f) seconds = 0f; + int total = (int)seconds; + int h = total / 3600; + int m = (total % 3600) / 60; + int s = total % 60; + + if (h > 0) + { + return string.Format(CultureInfo.InvariantCulture, "{0:D2}:{1:D2}:{2:D2}", h, m, s); + } + return string.Format(CultureInfo.InvariantCulture, "{0:D2}:{1:D2}", m, s); + } + + /// 0~1 범위의 확률을 퍼센트 문자열로 변환한다. 예: 0.1234 → "12.34%". + public static string FormatPercent(float ratio01, int decimals = 2) + { + float pct = ratio01 * 100f; + string format = "F" + decimals; + return pct.ToString(format, CultureInfo.InvariantCulture) + "%"; + } + + /// 수치 사이에 천 단위 구분자를 삽입한다(Invariant, 쉼표). + public static string WithThousands(long value) + { + return value.ToString("N0", CultureInfo.InvariantCulture); + } + + /// 바이트 크기를 B/KB/MB/GB 로 축약한다(Base 1024). + public static string FormatBytes(long bytes, int decimals = 2) + { + if (bytes < 0) return "-" + FormatBytes(-bytes, decimals); + string[] units = { "B", "KB", "MB", "GB", "TB" }; + double value = bytes; + int idx = 0; + while (value >= 1024.0 && idx < units.Length - 1) + { + value /= 1024.0; + idx++; + } + string format = idx == 0 ? "F0" : ("F" + decimals); + return value.ToString(format, CultureInfo.InvariantCulture) + units[idx]; + } + } +} diff --git a/코어코드/NerdNavis.Framework/Runtime/Core/Util/KeyMaker.cs b/코어코드/NerdNavis.Framework/Runtime/Core/Util/KeyMaker.cs new file mode 100644 index 0000000..d7a7175 --- /dev/null +++ b/코어코드/NerdNavis.Framework/Runtime/Core/Util/KeyMaker.cs @@ -0,0 +1,81 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework +// KeyMaker.cs — Dictionary·PlayerPrefs 키 조합 생성 유틸리티 +// --------------------------------------------------------------------------- +using System.Runtime.CompilerServices; + +namespace NerdNavis.Core.Util +{ + /// + /// Dictionary·PlayerPrefs·SaveData 등에서 사용할 합성 키를 일관된 규칙으로 생성하는 유틸리티. + /// + /// + /// 설계 문서 §5 Tier 1. 수상한 잡화점에서 `$"{category}_{id}"` 와 `$"{category}:{id}"` + /// 가 혼재되어 키 충돌·조회 실패가 발생한 경험을 반영, 프레임워크 전체에서 구분자 ':'로 고정한다. + /// 구분자 ':'를 선택한 이유: + /// + /// JSON·URL 경로·Redis 네임스페이스 등 외부 시스템 관례와 일치. + /// 숫자 ID·GUID 등에 포함되지 않아 파싱 안전. + /// PlayerPrefs 키로 안전하게 사용 가능. + /// + /// 주의: 이미 구분자를 포함한 값이 인자로 들어와도 이스케이프하지 않는다. + /// 키 역파싱()이 필요한 경우 호출자가 입력을 검증한다. + /// + public static class KeyMaker + { + /// 합성 키에 사용되는 구분자. + public const char Separator = ':'; + + /// 두 토큰으로 합성 키를 만든다. 예: Make("card", "101") → "card:101". + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Make(string a, string b) + { + return a + Separator + b; + } + + /// 세 토큰으로 합성 키를 만든다. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Make(string a, string b, string c) + { + return a + Separator + b + Separator + c; + } + + /// 문자열과 정수 ID로 합성 키를 만든다. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Make(string category, int id) + { + return category + Separator + id.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + + /// 문자열과 두 정수 ID로 합성 키를 만든다. 예: 슬롯별 장비. + public static string Make(string category, int idA, int idB) + { + return category + Separator + idA.ToString(System.Globalization.CultureInfo.InvariantCulture) + + Separator + idB.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + + /// 다수 토큰으로 합성 키를 만든다(원하는 수만큼 가변). 입력이 없으면 빈 문자열. + public static string MakeParams(params string[] tokens) + { + if (tokens == null || tokens.Length == 0) return string.Empty; + return string.Join(Separator.ToString(), tokens); + } + + /// 키의 최상위(첫 번째 구분자 앞) 토큰을 반환. 구분자가 없으면 전체 반환. + public static string SplitRoot(string key) + { + if (string.IsNullOrEmpty(key)) return string.Empty; + int idx = key.IndexOf(Separator); + return idx < 0 ? key : key.Substring(0, idx); + } + + /// 키에 가 붙어 있는지 확인한다(구분자 포함 체크). + public static bool StartsWithCategory(string key, string prefix) + { + if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(prefix)) return false; + if (!key.StartsWith(prefix)) return false; + if (key.Length == prefix.Length) return true; // 완전 일치도 동일 카테고리로 간주 + return key[prefix.Length] == Separator; + } + } +} diff --git a/코어코드/NerdNavis.Framework/Runtime/Core/Util/MathEx.cs b/코어코드/NerdNavis.Framework/Runtime/Core/Util/MathEx.cs new file mode 100644 index 0000000..b1f80c9 --- /dev/null +++ b/코어코드/NerdNavis.Framework/Runtime/Core/Util/MathEx.cs @@ -0,0 +1,110 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework +// MathEx.cs — 게임 빈출 수학 유틸리티 (Remap, Clamp, 부드러운 감쇠 등) +// --------------------------------------------------------------------------- +using System; +using System.Runtime.CompilerServices; + +namespace NerdNavis.Core.Util +{ + /// + /// UnityEngine.Mathf 에 없는 게임 빈출 수학 연산을 모은 유틸리티. + /// + /// + /// 설계 문서 §5 Tier 1. UnityEngine 참조 없이 순수 연산만 제공하므로 서버·배치 컨텍스트에서도 재사용 가능(C11 코드 범용성). + /// + public static class MathEx + { + /// 기준 작은 오차(1e-6). + public const float Epsilon = 1e-6f; + + /// 값을 [0,1]로 clamp한다. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Clamp01(float value) + { + if (value < 0f) return 0f; + if (value > 1f) return 1f; + return value; + } + + /// 값을 [min,max]로 clamp한다. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Clamp(int value, int min, int max) + { + if (value < min) return min; + if (value > max) return max; + return value; + } + + /// 값을 [min,max]로 clamp한다. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Clamp(float value, float min, float max) + { + if (value < min) return min; + if (value > max) return max; + return value; + } + + /// 입력 구간 [inMin,inMax] 의 값을 출력 구간 [outMin,outMax] 로 선형 리매핑한다. + /// 입력 구간이 0이면 을 반환한다(0-division 회피). + public static float Remap(float value, float inMin, float inMax, float outMin, float outMax) + { + float span = inMax - inMin; + if (span > -Epsilon && span < Epsilon) return outMin; + float t = (value - inMin) / span; + return outMin + (outMax - outMin) * t; + } + + /// 리매핑 후 출력 구간으로 clamp한다. + public static float RemapClamped(float value, float inMin, float inMax, float outMin, float outMax) + { + float mapped = Remap(value, inMin, inMax, outMin, outMax); + float lo = outMin < outMax ? outMin : outMax; + float hi = outMin < outMax ? outMax : outMin; + return Clamp(mapped, lo, hi); + } + + /// 두 실수가 이내로 근사하면 true. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Approximately(float a, float b, float epsilon = Epsilon) + { + float diff = a - b; + return diff > -epsilon && diff < epsilon; + } + + /// 프레임율 독립 부드러운 감쇠(Lerp with delta time). + /// 현재 값. + /// 목표 값. + /// 목표까지 절반 접근에 걸리는 시간(초). + /// 경과 시간(초). + /// halfLife·deltaTime이 0 이하이면 즉시 을 반환한다. + public static float SmoothTowards(float current, float target, float halfLife, float deltaTime) + { + if (halfLife <= 0f || deltaTime <= 0f) return target; + double k = Math.Pow(0.5, deltaTime / halfLife); + return target + (float)((current - target) * k); + } + + /// 두 정수의 최대공약수(GCD). 음수 입력은 절대값으로 처리. + public static int Gcd(int a, int b) + { + if (a < 0) a = -a; + if (b < 0) b = -b; + while (b != 0) + { + int t = b; + b = a % b; + a = t; + } + return a; + } + + /// 오버플로 없이 long으로 계산한 최소공배수(LCM). 둘 다 0이면 0. + public static long Lcm(int a, int b) + { + if (a == 0 || b == 0) return 0; + int g = Gcd(a, b); + return (long)(a / g) * b; + } + } +} diff --git a/코어코드/NerdNavis.Framework/Runtime/Core/Util/ValidationEx.cs b/코어코드/NerdNavis.Framework/Runtime/Core/Util/ValidationEx.cs new file mode 100644 index 0000000..22f82ec --- /dev/null +++ b/코어코드/NerdNavis.Framework/Runtime/Core/Util/ValidationEx.cs @@ -0,0 +1,93 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework +// ValidationEx.cs — 인자·입력 검증 헬퍼 (throw 규약 통일) +// --------------------------------------------------------------------------- +using System; +using System.Collections; + +namespace NerdNavis.Core.Util +{ + /// + /// 메서드 인자·상태 사전 검증을 위한 가드 헬퍼. + /// + /// + /// 설계 문서 §5 Tier 1. 프레임워크 전반에서 · + /// · 의 throw 규칙을 + /// 통일하여 호출자가 예측 가능한 예외 경로를 가질 수 있게 한다(C11 코드 직관성). + /// 성공 경로(값이 유효한 경우)는 원본 값을 그대로 반환하므로 체이닝 가능: + /// + /// this._name = ValidationEx.NotNull(name, nameof(name)); + /// + /// + public static class ValidationEx + { + /// 값이 null이면 을 던지고, 아니면 값을 반환한다. + public static T NotNull(T value, string paramName) where T : class + { + if (value == null) throw new ArgumentNullException(paramName); + return value; + } + + /// 문자열이 null이거나 빈 문자열이면 을 던진다. + public static string NotNullOrEmpty(string value, string paramName) + { + if (string.IsNullOrEmpty(value)) + throw new ArgumentException("must not be null or empty", paramName); + return value; + } + + /// 문자열이 null·빈 문자열·공백만 있으면 을 던진다. + public static string NotNullOrWhiteSpace(string value, string paramName) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("must not be null, empty, or whitespace", paramName); + return value; + } + + /// 컬렉션이 null이거나 원소가 0개면 을 던진다. + public static T NotNullOrEmpty(T collection, string paramName) where T : class, ICollection + { + if (collection == null) throw new ArgumentNullException(paramName); + if (collection.Count == 0) throw new ArgumentException("collection must not be empty", paramName); + return collection; + } + + /// 정수가 [min,max] 범위 밖이면 을 던진다. + public static int InRange(int value, int min, int max, string paramName) + { + if (value < min || value > max) + throw new ArgumentOutOfRangeException(paramName, value, $"must be in [{min},{max}]"); + return value; + } + + /// 실수가 [min,max] 범위 밖이면 을 던진다. + public static float InRange(float value, float min, float max, string paramName) + { + if (value < min || value > max) + throw new ArgumentOutOfRangeException(paramName, value, $"must be in [{min},{max}]"); + return value; + } + + /// 정수가 0보다 크지 않으면 을 던진다. + public static int Positive(int value, string paramName) + { + if (value <= 0) + throw new ArgumentOutOfRangeException(paramName, value, "must be > 0"); + return value; + } + + /// 정수가 음수이면 을 던진다(0 허용). + public static int NonNegative(int value, string paramName) + { + if (value < 0) + throw new ArgumentOutOfRangeException(paramName, value, "must be >= 0"); + return value; + } + + /// 조건이 false이면 을 던진다(상태 검증). + public static void State(bool condition, string message) + { + if (!condition) throw new InvalidOperationException(message); + } + } +} diff --git a/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Attribute/AttributeTests.cs b/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Attribute/AttributeTests.cs new file mode 100644 index 0000000..857114b --- /dev/null +++ b/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Attribute/AttributeTests.cs @@ -0,0 +1,70 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework.Tests +// AttributeTests.cs — ReadOnly/ShowIf/ArrayTitle 속성 단위 테스트 +// --------------------------------------------------------------------------- +using System; +using NUnit.Framework; +using NerdNavis.Core.Attribute; + +namespace NerdNavis.Tests.Core.Attribute +{ + public class AttributeTests + { + [Test] + public void ReadOnlyAttribute_IsSealedAndFieldOnly() + { + var t = typeof(ReadOnlyAttribute); + Assert.IsTrue(t.IsSealed); + var usage = (AttributeUsageAttribute)Attribute.GetCustomAttribute(t, typeof(AttributeUsageAttribute)); + Assert.IsNotNull(usage); + Assert.That(usage.ValidOn, Is.EqualTo(AttributeTargets.Field)); + Assert.IsFalse(usage.AllowMultiple); + } + + [Test] + public void ShowIfAttribute_TruthyConstructor_SetsMemberNameAndNullExpected() + { + var attr = new ShowIfAttribute("_flag"); + Assert.That(attr.MemberName, Is.EqualTo("_flag")); + Assert.IsNull(attr.ExpectedValue); + Assert.IsTrue(attr.HideInInspector); + } + + [Test] + public void ShowIfAttribute_ValueConstructor_SetsExpectedValue() + { + var attr = new ShowIfAttribute("_mode", 2); + Assert.That(attr.MemberName, Is.EqualTo("_mode")); + Assert.That(attr.ExpectedValue, Is.EqualTo(2)); + } + + [Test] + public void ShowIfAttribute_HideInInspectorOverridable() + { + var attr = new ShowIfAttribute("_flag", hideInInspector: false); + Assert.IsFalse(attr.HideInInspector); + } + + [Test] + public void ArrayTitleAttribute_DefaultPrefixIsEmpty() + { + var attr = new ArrayTitleAttribute("Name"); + Assert.That(attr.MemberName, Is.EqualTo("Name")); + Assert.That(attr.Prefix, Is.EqualTo(string.Empty)); + } + + [Test] + public void ArrayTitleAttribute_PrefixStored() + { + var attr = new ArrayTitleAttribute("Name", "Card"); + Assert.That(attr.Prefix, Is.EqualTo("Card")); + } + + [Test] + public void ArrayTitleAttribute_NullPrefixCoercedToEmpty() + { + var attr = new ArrayTitleAttribute("Name", null); + Assert.That(attr.Prefix, Is.EqualTo(string.Empty)); + } + } +} diff --git a/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/EnumExTests.cs b/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/EnumExTests.cs new file mode 100644 index 0000000..dc99a50 --- /dev/null +++ b/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/EnumExTests.cs @@ -0,0 +1,78 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework.Tests +// EnumExTests.cs — EnumEx 메타데이터·파싱 유틸 단위 테스트 +// --------------------------------------------------------------------------- +using NUnit.Framework; +using NerdNavis.Core.Util; + +namespace NerdNavis.Tests.Core.Util +{ + public class EnumExTests + { + private enum Color { Red = 1, Green = 2, Blue = 4 } + + [Test] + public void GetValues_ReturnsAllDefinedValues() + { + var values = EnumEx.GetValues(); + Assert.That(values.Length, Is.EqualTo(3)); + CollectionAssert.AreEquivalent(new[] { Color.Red, Color.Green, Color.Blue }, values); + } + + [Test] + public void GetValues_ReturnsCachedInstance() + { + var first = EnumEx.GetValues(); + var second = EnumEx.GetValues(); + Assert.AreSame(first, second, "배열 재할당이 발생하면 hot-path 비용이 증가한다"); + } + + [Test] + public void GetNames_ReturnsDefinedNames() + { + var names = EnumEx.GetNames(); + CollectionAssert.AreEquivalent(new[] { "Red", "Green", "Blue" }, names); + } + + [Test] + public void Count_ReturnsDefinedValueCount() + { + Assert.That(EnumEx.Count(), Is.EqualTo(3)); + } + + [Test] + public void IsDefined_TrueForDefinedValue() + { + Assert.IsTrue(EnumEx.IsDefined(Color.Green)); + } + + [Test] + public void IsDefined_FalseForUndefinedIntCastValue() + { + Color invalid = (Color)999; + Assert.IsFalse(EnumEx.IsDefined(invalid)); + } + + [Test] + public void TryParse_CaseInsensitiveByDefault() + { + Assert.IsTrue(EnumEx.TryParse("green", out var result)); + Assert.That(result, Is.EqualTo(Color.Green)); + } + + [Test] + public void TryParse_ReturnsFalseForNullOrEmpty() + { + Assert.IsFalse(EnumEx.TryParse("", out _)); + Assert.IsFalse(EnumEx.TryParse(null, out _)); + } + + [Test] + public void TryParse_RespectsCaseSensitivity() + { + Assert.IsFalse(EnumEx.TryParse("green", ignoreCase: false, out _)); + Assert.IsTrue(EnumEx.TryParse("Green", ignoreCase: false, out var r)); + Assert.That(r, Is.EqualTo(Color.Green)); + } + } +} diff --git a/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/EnumToIntTests.cs b/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/EnumToIntTests.cs new file mode 100644 index 0000000..b2eca55 --- /dev/null +++ b/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/EnumToIntTests.cs @@ -0,0 +1,46 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework.Tests +// EnumToIntTests.cs — EnumToInt 박싱-프리 변환 단위 테스트 +// --------------------------------------------------------------------------- +using NUnit.Framework; +using NerdNavis.Core.Util; + +namespace NerdNavis.Tests.Core.Util +{ + public class EnumToIntTests + { + private enum Sample { A = 0, B = 1, C = 42, Neg = -5 } + + [Test] + public void Convert_ReturnsUnderlyingIntValue() + { + Assert.That(EnumToInt.Convert(Sample.A), Is.EqualTo(0)); + Assert.That(EnumToInt.Convert(Sample.B), Is.EqualTo(1)); + Assert.That(EnumToInt.Convert(Sample.C), Is.EqualTo(42)); + Assert.That(EnumToInt.Convert(Sample.Neg), Is.EqualTo(-5)); + } + + [Test] + public void FromInt_ReturnsMatchingEnum() + { + Assert.That(EnumToInt.FromInt(42), Is.EqualTo(Sample.C)); + Assert.That(EnumToInt.FromInt(-5), Is.EqualTo(Sample.Neg)); + } + + [Test] + public void Equals_ComparesEnumValuesWithoutBoxing() + { + Assert.IsTrue(EnumToInt.Equals(Sample.C, Sample.C)); + Assert.IsFalse(EnumToInt.Equals(Sample.A, Sample.B)); + } + + [Test] + public void RoundTrip_PreservesValue() + { + Sample original = Sample.C; + int id = EnumToInt.Convert(original); + Sample restored = EnumToInt.FromInt(id); + Assert.That(restored, Is.EqualTo(original)); + } + } +} diff --git a/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/FormatExTests.cs b/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/FormatExTests.cs new file mode 100644 index 0000000..10e8174 --- /dev/null +++ b/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/FormatExTests.cs @@ -0,0 +1,79 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework.Tests +// FormatExTests.cs — FormatEx 포맷 유틸 단위 테스트 +// --------------------------------------------------------------------------- +using NUnit.Framework; +using NerdNavis.Core.Util; + +namespace NerdNavis.Tests.Core.Util +{ + public class FormatExTests + { + [Test] + public void AbbreviateNumber_BelowThousand_ReturnsPlain() + { + Assert.That(FormatEx.AbbreviateNumber(0), Is.EqualTo("0")); + Assert.That(FormatEx.AbbreviateNumber(42), Is.EqualTo("42")); + Assert.That(FormatEx.AbbreviateNumber(999), Is.EqualTo("999")); + } + + [Test] + public void AbbreviateNumber_UsesKMBTiers() + { + Assert.That(FormatEx.AbbreviateNumber(12_345), Is.EqualTo("12.3K")); + Assert.That(FormatEx.AbbreviateNumber(1_500_000), Is.EqualTo("1.5M")); + Assert.That(FormatEx.AbbreviateNumber(2_000_000_000), Is.EqualTo("2.0B")); + } + + [Test] + public void AbbreviateNumber_NegativeKeepsSign() + { + Assert.That(FormatEx.AbbreviateNumber(-1_500), Is.EqualTo("-1.5K")); + } + + [Test] + public void AbbreviateNumber_ZeroDecimalsStripsFraction() + { + Assert.That(FormatEx.AbbreviateNumber(1_500, decimals: 0), Is.EqualTo("2K")); + } + + [Test] + public void FormatTimeSpan_UnderHour_MinSecOnly() + { + Assert.That(FormatEx.FormatTimeSpan(65f), Is.EqualTo("01:05")); + Assert.That(FormatEx.FormatTimeSpan(0f), Is.EqualTo("00:00")); + } + + [Test] + public void FormatTimeSpan_OverHour_IncludesHour() + { + Assert.That(FormatEx.FormatTimeSpan(3670f), Is.EqualTo("01:01:10")); + } + + [Test] + public void FormatTimeSpan_NegativeClampsToZero() + { + Assert.That(FormatEx.FormatTimeSpan(-5f), Is.EqualTo("00:00")); + } + + [Test] + public void FormatPercent_TwoDecimalsByDefault() + { + Assert.That(FormatEx.FormatPercent(0.1234f), Is.EqualTo("12.34%")); + } + + [Test] + public void WithThousands_InsertsCommas() + { + Assert.That(FormatEx.WithThousands(1_234_567), Is.EqualTo("1,234,567")); + } + + [Test] + public void FormatBytes_ScalesTo1024() + { + Assert.That(FormatEx.FormatBytes(512), Is.EqualTo("512B")); + Assert.That(FormatEx.FormatBytes(2048), Is.EqualTo("2.00KB")); + Assert.That(FormatEx.FormatBytes(1_048_576), Is.EqualTo("1.00MB")); + } + } +} diff --git a/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/KeyMakerTests.cs b/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/KeyMakerTests.cs new file mode 100644 index 0000000..e86178d --- /dev/null +++ b/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/KeyMakerTests.cs @@ -0,0 +1,70 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework.Tests +// KeyMakerTests.cs — KeyMaker 합성 키 단위 테스트 +// --------------------------------------------------------------------------- +using NUnit.Framework; +using NerdNavis.Core.Util; + +namespace NerdNavis.Tests.Core.Util +{ + public class KeyMakerTests + { + [Test] + public void Separator_IsColon() + { + Assert.That(KeyMaker.Separator, Is.EqualTo(':')); + } + + [Test] + public void Make_TwoStrings_JoinsWithSeparator() + { + Assert.That(KeyMaker.Make("card", "101"), Is.EqualTo("card:101")); + } + + [Test] + public void Make_StringAndInt_UsesInvariantFormat() + { + Assert.That(KeyMaker.Make("card", 101), Is.EqualTo("card:101")); + Assert.That(KeyMaker.Make("card", -5), Is.EqualTo("card:-5")); + } + + [Test] + public void Make_ThreeTokens_JoinsAll() + { + Assert.That(KeyMaker.Make("a", "b", "c"), Is.EqualTo("a:b:c")); + } + + [Test] + public void MakeParams_VarArgsJoin() + { + Assert.That(KeyMaker.MakeParams("a", "b", "c", "d"), Is.EqualTo("a:b:c:d")); + Assert.That(KeyMaker.MakeParams(), Is.EqualTo(string.Empty)); + Assert.That(KeyMaker.MakeParams(null), Is.EqualTo(string.Empty)); + } + + [Test] + public void SplitRoot_ReturnsFirstToken() + { + Assert.That(KeyMaker.SplitRoot("card:101"), Is.EqualTo("card")); + Assert.That(KeyMaker.SplitRoot("card:101:x"), Is.EqualTo("card")); + } + + [Test] + public void SplitRoot_NoSeparator_ReturnsWhole() + { + Assert.That(KeyMaker.SplitRoot("card"), Is.EqualTo("card")); + Assert.That(KeyMaker.SplitRoot(""), Is.EqualTo("")); + Assert.That(KeyMaker.SplitRoot(null), Is.EqualTo("")); + } + + [Test] + public void StartsWithCategory_RequiresSeparatorBoundary() + { + Assert.IsTrue(KeyMaker.StartsWithCategory("card:101", "card")); + Assert.IsTrue(KeyMaker.StartsWithCategory("card", "card")); + // prefix가 substring만 일치하는 경우는 false (card ≠ cards) + Assert.IsFalse(KeyMaker.StartsWithCategory("cards:101", "card")); + Assert.IsFalse(KeyMaker.StartsWithCategory("", "card")); + } + } +} diff --git a/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/MathExTests.cs b/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/MathExTests.cs new file mode 100644 index 0000000..40adcfe --- /dev/null +++ b/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/MathExTests.cs @@ -0,0 +1,94 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework.Tests +// MathExTests.cs — MathEx 수학 유틸 단위 테스트 +// --------------------------------------------------------------------------- +using NUnit.Framework; +using NerdNavis.Core.Util; + +namespace NerdNavis.Tests.Core.Util +{ + public class MathExTests + { + [Test] + public void Clamp01_BoundsToZeroOne() + { + Assert.That(MathEx.Clamp01(-0.5f), Is.EqualTo(0f)); + Assert.That(MathEx.Clamp01(1.5f), Is.EqualTo(1f)); + Assert.That(MathEx.Clamp01(0.5f), Is.EqualTo(0.5f)); + } + + [Test] + public void Clamp_IntAndFloatHonorBounds() + { + Assert.That(MathEx.Clamp(5, 0, 10), Is.EqualTo(5)); + Assert.That(MathEx.Clamp(-1, 0, 10), Is.EqualTo(0)); + Assert.That(MathEx.Clamp(11, 0, 10), Is.EqualTo(10)); + Assert.That(MathEx.Clamp(1.5f, 0f, 1f), Is.EqualTo(1f)); + } + + [Test] + public void Remap_LinearMappingWorks() + { + // [0,100] -> [0,1] + Assert.That(MathEx.Remap(50f, 0f, 100f, 0f, 1f), Is.EqualTo(0.5f).Within(MathEx.Epsilon)); + // [0,100] -> [-1,1] + Assert.That(MathEx.Remap(0f, 0f, 100f, -1f, 1f), Is.EqualTo(-1f).Within(MathEx.Epsilon)); + Assert.That(MathEx.Remap(100f, 0f, 100f, -1f, 1f), Is.EqualTo(1f).Within(MathEx.Epsilon)); + } + + [Test] + public void Remap_ZeroSpan_ReturnsOutMin() + { + // 입력 범위가 0이면 0-division 없이 outMin 반환 + Assert.That(MathEx.Remap(5f, 3f, 3f, -10f, 10f), Is.EqualTo(-10f)); + } + + [Test] + public void RemapClamped_ClipsOutsideInputRange() + { + Assert.That(MathEx.RemapClamped(-50f, 0f, 100f, 0f, 1f), Is.EqualTo(0f)); + Assert.That(MathEx.RemapClamped(150f, 0f, 100f, 0f, 1f), Is.EqualTo(1f)); + } + + [Test] + public void Approximately_WithinEpsilonReturnsTrue() + { + Assert.IsTrue(MathEx.Approximately(1.0f, 1.0f + 1e-7f)); + Assert.IsFalse(MathEx.Approximately(1.0f, 1.1f)); + } + + [Test] + public void SmoothTowards_ReachesTargetEventually() + { + float current = 0f; + for (int i = 0; i < 1000; i++) + { + current = MathEx.SmoothTowards(current, 10f, halfLife: 0.1f, deltaTime: 0.1f); + } + Assert.That(current, Is.EqualTo(10f).Within(0.01f)); + } + + [Test] + public void SmoothTowards_ZeroOrNegativeHalfLife_SnapsToTarget() + { + Assert.That(MathEx.SmoothTowards(0f, 10f, halfLife: 0f, deltaTime: 0.1f), Is.EqualTo(10f)); + Assert.That(MathEx.SmoothTowards(0f, 10f, halfLife: -1f, deltaTime: 0.1f), Is.EqualTo(10f)); + } + + [Test] + public void Gcd_BasicCases() + { + Assert.That(MathEx.Gcd(12, 18), Is.EqualTo(6)); + Assert.That(MathEx.Gcd(17, 13), Is.EqualTo(1)); + Assert.That(MathEx.Gcd(-12, 18), Is.EqualTo(6)); + Assert.That(MathEx.Gcd(0, 5), Is.EqualTo(5)); + } + + [Test] + public void Lcm_BasicCases() + { + Assert.That(MathEx.Lcm(4, 6), Is.EqualTo(12)); + Assert.That(MathEx.Lcm(0, 5), Is.EqualTo(0)); + } + } +} diff --git a/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/ValidationExTests.cs b/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/ValidationExTests.cs new file mode 100644 index 0000000..1dd15e3 --- /dev/null +++ b/코어코드/NerdNavis.Framework/Tests/Runtime/Core/Util/ValidationExTests.cs @@ -0,0 +1,85 @@ +// --------------------------------------------------------------------------- +// NerdNavis.Framework.Tests +// ValidationExTests.cs — ValidationEx 가드 단위 테스트 +// --------------------------------------------------------------------------- +using System; +using System.Collections.Generic; +using NUnit.Framework; +using NerdNavis.Core.Util; + +namespace NerdNavis.Tests.Core.Util +{ + public class ValidationExTests + { + [Test] + public void NotNull_NonNull_ReturnsValue() + { + var obj = new object(); + Assert.AreSame(obj, ValidationEx.NotNull(obj, "x")); + } + + [Test] + public void NotNull_Null_Throws() + { + Assert.Throws(() => ValidationEx.NotNull(null, "x")); + } + + [Test] + public void NotNullOrEmpty_String_ThrowsForEmpty() + { + Assert.Throws(() => ValidationEx.NotNullOrEmpty("", "x")); + Assert.Throws(() => ValidationEx.NotNullOrEmpty((string)null, "x")); + Assert.That(ValidationEx.NotNullOrEmpty("abc", "x"), Is.EqualTo("abc")); + } + + [Test] + public void NotNullOrWhiteSpace_ThrowsForWhitespace() + { + Assert.Throws(() => ValidationEx.NotNullOrWhiteSpace(" ", "x")); + Assert.That(ValidationEx.NotNullOrWhiteSpace("abc", "x"), Is.EqualTo("abc")); + } + + [Test] + public void NotNullOrEmpty_Collection_ThrowsForEmpty() + { + var empty = new List(); + Assert.Throws(() => ValidationEx.NotNullOrEmpty(empty, "x")); + + var nonEmpty = new List { 1 }; + Assert.AreSame(nonEmpty, ValidationEx.NotNullOrEmpty(nonEmpty, "x")); + } + + [Test] + public void InRange_Int_BoundsChecked() + { + Assert.That(ValidationEx.InRange(5, 0, 10, "x"), Is.EqualTo(5)); + Assert.That(ValidationEx.InRange(0, 0, 10, "x"), Is.EqualTo(0)); + Assert.That(ValidationEx.InRange(10, 0, 10, "x"), Is.EqualTo(10)); + Assert.Throws(() => ValidationEx.InRange(-1, 0, 10, "x")); + Assert.Throws(() => ValidationEx.InRange(11, 0, 10, "x")); + } + + [Test] + public void Positive_ThrowsForZeroAndNegative() + { + Assert.That(ValidationEx.Positive(1, "x"), Is.EqualTo(1)); + Assert.Throws(() => ValidationEx.Positive(0, "x")); + Assert.Throws(() => ValidationEx.Positive(-1, "x")); + } + + [Test] + public void NonNegative_AllowsZero() + { + Assert.That(ValidationEx.NonNegative(0, "x"), Is.EqualTo(0)); + Assert.That(ValidationEx.NonNegative(1, "x"), Is.EqualTo(1)); + Assert.Throws(() => ValidationEx.NonNegative(-1, "x")); + } + + [Test] + public void State_ThrowsInvalidOperationOnFalse() + { + Assert.Throws(() => ValidationEx.State(false, "bad")); + Assert.DoesNotThrow(() => ValidationEx.State(true, "ok")); + } + } +} diff --git a/프로젝트/수상한잡화점/개발/11_UI아키텍처_v1.md b/프로젝트/수상한잡화점/개발/11_UI아키텍처_v1.md new file mode 100644 index 0000000..c209fe1 --- /dev/null +++ b/프로젝트/수상한잡화점/개발/11_UI아키텍처_v1.md @@ -0,0 +1,166 @@ +# 11. UI 아키텍처 — 수상한 잡화점 + +> **작성일**: 2026-04-17 +> **작성자**: 개발팀장 +> **상태**: v1 (초판, Phase 0-B 연계 문서) +> **대상**: `Assets/Script/UGUI/` 전수 (Ingame 19 + Lobby 19 + Common/Title/Util/Manager/BackKey 등) +> **관련 문서**: `08_전투시스템_SOT_v1.md`, `09_카드시스템_아키텍처_v1.md`, `10_데이터로딩_구조_v1.md` + +--- + +## 1. 목적 (P18 §결정의 배경) + +수상한 잡화점 UI 계층의 **구조·주요 매니저·상호작용 패턴**을 전수 식별하여: +1. 전투·카드·데이터 문서(08·09·10)에 이은 Phase 0-B 완결 (UI는 기획 밸런싱·시뮬레이션의 최종 표시 계층) +2. 코어 프레임워크 Tier 1 `NerdNavis.UI.UGUI`·`NerdNavis.UI.Components`에 흡수할 **범용 패턴 vs 프로젝트 특수 로직** 경계 확정 (C11) +3. UI 기획 연동·UX 검증 작업 시 영향 범위 식별에 필요한 **단일 SOT** + +## 2. 전체 계층 구조 + +``` +UGUI/ +├── Manager/ UI 전역 매니저 계층 — 씬·팝업·로딩 브릿지 +├── BackKey/ Android 뒤로가기 통합 처리 +├── Common/ 씬 공용 (GameUI, ScenarioUI, ToastUI, ControlUI 등) +├── Util/ SafeArea, UI 확장 컴포넌트 +├── Title/ 타이틀·로딩·공지·계정 진입 흐름 +├── Lobby/ 로비 씬 UI (19개 스크립트) +│ ├── LobbyUIManager ← 로비 최상위 오케스트레이터 +│ ├── LobbyTopUI, MoneyCard +│ ├── MainMenu/ 영웅·카드·장비·미션·상점 (Base + 5개 탭) +│ ├── Attandance/ 출석 보상 +│ ├── CatTrade/ 상점(고양이 상인) — 재화·장비·인장·메인 미션 통합 (11종) +│ ├── Explore/ 탐험 — 지도·스테이지·보상 (7종) +│ ├── SeasonPass/ 시즌 패스 (2종) +│ └── ETC/ +├── Ingame/ 전투·던전 씬 UI (19개 스크립트) +│ ├── IngameUIManager ← 인게임 최상위 오케스트레이터 (153 LOC) +│ ├── DungeonProcess ← 스테이지 진행 UI (67 LOC) +│ ├── SelectCardUI ← 카드 선택 (290 LOC, 최대 UI 단일 모듈) +│ ├── BattleCard, BattleSmallCard, DeckUI, DeckUI_Skills, DeckUI_My4Spec +│ ├── GainCardList, IngameTopUI, SpecificityCard/List +│ ├── MerchantBuyPopup, PCMainStatUI, IngameMainStatCard +│ ├── Result/ DefeatUI, WinUI, StageClearRewards, ResultComon +│ └── ETC/ +└── zTestUI/ +``` + +## 3. 최상위 매니저 3종 — 오케스트레이션 책임 + +### 3-1. `LobbyUIManager` (Lobby/LobbyUIManager.cs, 202 LOC) +- 로비 씬 전체 UI 탭 전환·활성화 제어 +- 탭 구성: MainMenu(영웅/카드/장비/미션/상점) · Attandance · CatTrade · Explore · SeasonPass +- 각 탭 자체 열림 상태·재진입 초기화 책임은 하위 UI에 위임하되, **탭 간 배타 활성은 본 매니저가 통제** + +### 3-2. `IngameUIManager` (Ingame/IngameUIManager.cs, 153 LOC) +- 인게임(전투) 씬 UI 전역 오케스트레이터 +- 주요 책임: `DungeonProcess` 연동, 전투 카드 스폰/회수, 결과 UI(Result/) 트리거 +- 08 전투시스템 SOT 문서의 FSM 상태 전환 이벤트를 구독하여 UI 표시 갱신 + +### 3-3. `Title_Mgr` (Manager/Title_Mgr.cs) +- 타이틀 씬 진입 — 로딩·공지·계정 인증 흐름 제어 +- `AddrResourceMgr`·`DataCheckMgr`와 협력 (Addressable 로드·마스터 데이터 체크) + +## 4. 공통 UI 컴포넌트 (Common + Util) + +| 컴포넌트 | 위치 | 범용성 | 프레임워크 흡수 대상 | +|---------|------|--------|--------------------| +| `GameUI` | Common/GameUI.cs | ★ 매우 높음 (UIView 추상화 출발점) | ✅ `NerdNavis.UI.UGUI.UIView` | +| `ScenarioUI` | Common/ScenarioUI.cs | △ 프로젝트 특수 (시나리오 시스템) | ❌ 프로젝트 유지 | +| `ToastUI` | Common/ToastUI.cs | ★ 매우 높음 | ✅ `NerdNavis.UI.Components` | +| `TouchBlockUI` | Common/TouchBlockUI.cs | ★ 매우 높음 | ✅ `NerdNavis.UI.Components` | +| `ControlUI`/`ControlUILock` | Common/ | ○ UI 입력 잠금 패턴 | ✅ `NerdNavis.UI.Components` | +| `MainStatCardBase` | Common/MainStatCardBase.cs | △ 프로젝트 카드 베이스 | ❌ 프로젝트 유지 | +| `ItemSimpleCard` | Common/ItemSimpleCard.cs | △ 프로젝트 아이템 카드 | ❌ 프로젝트 유지 | +| `SafeArea` | Util/ (추정) | ★ 매우 높음 | ✅ `NerdNavis.UI.Components.SafeArea` | +| `UIAtlasMgr` | UGUI/Manager/uScrollViewMgr 계열 | ★ 높음 | ✅ `NerdNavis.UI.UGUI.AtlasManager` | +| 무한 스크롤 (`uScrollViewMgr`·`uScrollViewArrMgr`) | UGUI/Manager/ | ★ 높음 | ✅ `NerdNavis.UI.UGUI.VirtualScroll` | + +> 범용성 표기: ★=1순위 흡수 · ○=2순위 · △=프로젝트 특수 · ❌=범용 불가 + +## 5. 핵심 UI 모듈 — 카드 중심 설계 + +### 5-1. BattleCard / BattleSmallCard (Ingame) +- 전투 중 실제 표시되는 카드 UI. 09 카드시스템 아키텍처의 런타임 표시 계층 +- 지속 인스턴스 풀링 대상 (프레임 당 다수 스폰/회수) + +### 5-2. SelectCardUI (Ingame, 290 LOC — 최대 단일 UI 모듈) +- 카드 선택(획득 후보 3장 중 1장 선택) UI +- 제약 조건(C7 재미): 선택 시간·UX 피드백·희귀도 연출이 집약됨 +- **프레임워크 흡수 비대상** — 프로젝트 특수 선택 규칙 (배제 조건·카드 풀 계산 등) + +### 5-3. DeckUI 계열 (DeckUI, DeckUI_Skills, DeckUI_My4Spec) +- 데크·스킬·4스펙(고유 특성) 표시 UI +- 슬롯 기반 그리드 배치 패턴 → VirtualScroll·GridPool 흡수 후보 + +### 5-4. DungeonProcess (Ingame, 67 LOC) +- 스테이지 진행 UI — 맵 패턴·배틀·이벤트·보스 표시 +- 맵 패턴 규칙(P17·기획팀 스테이지 설계)의 UI 표현 계층 + +## 6. 로비 기능별 UI 클러스터 + +### 6-1. MainMenu — 영웅·카드·장비·미션·상점 (5탭) +- `Base/`: 공통 베이스 +- `1_Hero`·`2_Card`·`3_Equipment`·`4_Mission`: 각 탭 세부 (숫자 접두어 = 표시 순서) +- `MainMenu_Shop.cs`: 상점 탭(본 스크립트는 `5_Shop` 폴더가 아닌 루트에 존재 — 폴더 미정립) + +### 6-2. CatTrade — 상점(고양이 상인) 통합 (11개 스크립트, 로비 내 최대 클러스터) +- 재화 거래(`CatTradeUI_Goods`) +- 장비 거래(`CatTradeUI_Equipment` + `CatTradeEquipmentBuyPopup` + `EquipmentTradeInvenCard`) +- 메인 미션(`CatTradeUI_MainMission`) +- 인장(`CatTradeUI_Seal` + `GetSealUI` + `SealRoulleteCard` + `SealSlotScroller`) +- 재화 카드(`CatTradeGoodsCard`) + +### 6-3. Explore — 탐험 (7개 스크립트) +- 맵(`ExploreUI_Map`·`ExploreMapName`·`ExploreUI_Map_Cloud`(안개)): 맵 노드·구름 연출 +- 스테이지(`ExploreUI_StageSelect`·`ExploreStageCard`): 노드 선택·카드 표시 +- 보상(`ExploreRewardPopup`) + +### 6-4. SeasonPass · Attandance +- 시즌 패스(`SeasonPassUI`·`SeasonPassCard`): 트랙·레벨·보상 표시 +- 출석(`AttandanceUI`·`AttandanceCard`): 일일 보상 슬롯 + +## 7. BackKey 통합 처리 + +- `BackKey/` 디렉토리: Android `Escape` 키 통합 처리 +- 열린 UI 스택 관리 → 가장 위 UI부터 순차 닫기 +- **프레임워크 흡수 후보**: `NerdNavis.UI.UGUI.BackKeyDispatcher` (안드로이드 필수 패턴) + +## 8. 프레임워크 Tier 1 흡수 계획 (범용성 ★ 우선) + +차기 프로젝트 활용을 위한 **범용 UI 프레임워크** 구성 요소: + +| 모듈 | 네임스페이스 | 수상한 잡화점 출처 | +|------|------------|------------------| +| UIView 추상 베이스 | `NerdNavis.UI.UGUI.UIView` | `Common/GameUI` | +| SafeArea | `NerdNavis.UI.Components.SafeArea` | `UGUI/Util/SafeArea` | +| Toast | `NerdNavis.UI.Components.Toast` | `Common/ToastUI` | +| 입력 차단 | `NerdNavis.UI.Components.InputBlocker` | `Common/TouchBlockUI`·`ControlUI` | +| 아틀라스 매니저 | `NerdNavis.UI.UGUI.AtlasManager` | `UGUI/Manager/UIAtlasMgr` | +| 무한 스크롤 | `NerdNavis.UI.UGUI.VirtualScroll` | `UGUI/Manager/uScrollView*Mgr` | +| BackKey 디스패처 | `NerdNavis.UI.UGUI.BackKeyDispatcher` | `BackKey/` | + +## 9. 기획 연동 포인트 (UX 검증 시 영향 범위) + +| 기획 변경 범주 | 영향 UI 모듈 | +|---------------|------------| +| 카드 조건·배타(P17) | `SelectCardUI`, `SpecificityCard/List` | +| 스테이지 맵 패턴 | `DungeonProcess`, `ExploreUI_Map`·`ExploreUI_StageSelect` | +| 보상 수치 | `ExploreRewardPopup`, `StageClearRewards`, `Result/WinUI` | +| 상점 재화 밸런스 | `CatTradeUI_*`, `MerchantBuyPopup`, `MoneyCard` | +| 시즌 패스 보상 트랙 | `SeasonPassUI`, `SeasonPassCard` | +| 출석 보상 | `AttandanceUI`, `AttandanceCard` | + +## 10. 검증 방법 (P18 §검증) + +1. 각 행의 파일 경로는 `Assets/Script/UGUI/{클러스터}/{파일명}.cs`에서 실존 확인 가능 (본 문서 작성 시 `ls`로 전수 확인 완료) +2. LOC 표기는 `wc -l` 결과 기준 +3. 범용성 분류(★/○/△/❌)는 의존성 스캔(Unity 타입·게임 특수 enum 참조 여부)로 재검증 가능 + +## 11. 기각안 (P24 §기각안) + +- **기각안 A: UIToolkit 병행 매핑 문서화** — 기획 방향이 UGUI 단일이므로 UIToolkit 매핑은 차기 프로젝트 R&D로 이관 (본 문서는 UGUI 전수 한정) +- **기각안 B: 각 스크립트 public API 메서드 전수 목록화** — 토큰 비용 과다 + 기획·검증 영향도 분류 목적에 불필요. 구조 수준 분류로 대체 + +## 부록 A. 변경 이력 +- **v1 (2026-04-17)**: 초판. 개발팀장 Phase 0-B 완결 작업(B-4)으로 작성. Assets 전수 `ls` + 주요 파일 LOC 실측 기반. diff --git a/프로젝트/수상한잡화점/개발/12_메타시스템_v1.md b/프로젝트/수상한잡화점/개발/12_메타시스템_v1.md new file mode 100644 index 0000000..cc1cc8f --- /dev/null +++ b/프로젝트/수상한잡화점/개발/12_메타시스템_v1.md @@ -0,0 +1,204 @@ +# 12. 메타시스템 — 수상한 잡화점 + +> **작성일**: 2026-04-17 +> **작성자**: 개발팀장 +> **상태**: v1 (초판, Phase 0-B 연계 문서) +> **대상**: 세이브/로드 · 진행도 · 상점 · 성장(장비·각성·인장) · 시즌패스 · 탐험 · 출석 +> **관련 문서**: `05_서버연동_현황_v1.md`, `09_카드시스템_아키텍처_v1.md`, `10_데이터로딩_구조_v1.md`, `11_UI아키텍처_v1.md` + +--- + +## 1. 목적 (P18 §결정의 배경) + +수상한 잡화점의 **비전투(메타) 시스템** 전체 계층을 식별하여: +1. 전투(08) · 카드(09) · 데이터(10) · UI(11)에 이은 Phase 0-B 최종 완결 +2. 서버 역할 문서(05·PD 지시 #30·#31)와의 **메타 시스템 측 대응표** 제공 +3. 차기 프로젝트 프레임워크 `NerdNavis.Save`·`NerdNavis.Economy` Tier 2 설계에 필요한 **실증 패턴** 공급 +4. 기획 밸런싱·유저 경험 변경 시 **영향 범위 식별 SOT** + +## 2. 메타시스템 전체 맵 + +``` +메타시스템 = "전투 외 유저 진행·성장·경제 시스템" + +┌─────────────────────────────────────────────────────────────┐ +│ 클라이언트 로컬 상태 (Assets/Script/Info/) │ +│ ├── ActorInfo — 캐릭터·영웅 상태 │ +│ ├── InGameInfo — 인게임 진행 상태 │ +│ ├── InappInfo — 인앱 결제 영수증·트랜잭션 │ +│ ├── OptionInfo — 유저 옵션(사운드·언어·그래픽) │ +│ ├── TitleInfo — 타이틀·공지 │ +│ ├── ADInfo — 광고 상태 │ +│ ├── SoundInfo, WebViewInfo, UtilInfo, Popup, NetWait │ +│ └── (*.Info 단일 데이터 클래스 + 매니저 스타일) │ +└─────────────────────────────────────────────────────────────┘ + ↕ (마스터 테이블 조회, 10 문서) +┌─────────────────────────────────────────────────────────────┐ +│ 서버 연동 (Assets/Script/Server/) │ +│ ├── ServerClass.cs — 서버 응답/요청 DTO │ +│ ├── ServerInfo.cs — 서버 상태·세션 관리 │ +│ └── (PlayFab 전제, 05 문서 및 서버 지시서 v1.1/v1.2 참조) │ +└─────────────────────────────────────────────────────────────┘ + ↕ (UI 표시, 11 문서) +┌─────────────────────────────────────────────────────────────┐ +│ UI 계층 (Assets/Script/UGUI/Lobby/) │ +│ └── 본 문서 §4 각 시스템별 UI 매핑 │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 3. 세이브/로드 구조 + +### 3-1. 현 구조 (PlayFab 중심) +- **원본**: PlayFab(`UserData`/`TitleData`/`PlayerStatistics`) — 서버 지시서 v1.1 §4·§5 기준 +- **로컬 캐시**: `Info/*.Info` 각 클래스 내부에 필드 보존 + 필요 시 `PlayerPrefs` 부분 저장(옵션·로컬 전용) +- **인코딩**: JSON(PlayFab 기본 직렬화) + `My/CryptoUtil.cs` 적용분은 일부 (세이브 전반 체계화 안 됨) + +### 3-2. 약점·리스크 +1. **SOT 분리 모호** — `*.Info` 클래스가 "런타임 상태 + 저장 대상" 이중 역할 +2. **세이브 스키마 버전 관리 부재** — 필드 추가/제거 시 마이그레이션 전략 없음 +3. **로컬/서버 동기화 타이밍이 호출부에 산재** — 조회 시마다 서버 호출 or 캐시 사용 판단 분산 + +### 3-3. Tier 2 `NerdNavis.Save` 설계 반영 포인트 +- `ISaveProvider` 인터페이스 (PlayerPrefs / JSON / 암호화 / 클라우드) +- 버전 마이그레이션 훅(`IMigration`) 신설 +- 세이브 대상 POCO와 런타임 상태 클래스 분리 (DTO 패턴) + +## 4. 진행도 · 성장 · 경제 시스템 (영역별) + +### 4-1. 영웅·카드·장비·각성·인장 (성장) + +| 시스템 | 클라 코드 위치 | UI 클러스터 | 마스터 테이블 | +|--------|--------------|-----------|-------------| +| 영웅 (Hero) | `Info/ActorInfo.cs` + 스킬·스펙 서브 | `Lobby/MainMenu/1_Hero/` | Hero, Skill, HeroSkill | +| 카드 (Card) | 09 문서 런타임 모듈 + `Info/` | `Lobby/MainMenu/2_Card/` | Card, CardEffect | +| 장비 (Equipment) | `Info/`·CatTrade 관련 | `Lobby/MainMenu/3_Equipment/`·`Lobby/CatTrade/CatTradeUI_Equipment.cs` | Equipment, EquipmentOption | +| 미션 (Mission) | `Info/`·`CatTradeUI_MainMission` | `Lobby/MainMenu/4_Mission/`·`Lobby/CatTrade/CatTradeUI_MainMission.cs` | Mission, MissionReward | +| 인장 (Seal) | CatTrade Seal 서브 | `Lobby/CatTrade/CatTradeUI_Seal.cs`·`GetSealUI.cs`·`SealRoulleteCard.cs`·`SealSlotScroller.cs` | Seal, SealOption | + +### 4-2. 상점 (CatTrade — 고양이 상인) +- **구조**: 단일 상인이 5개 카테고리(재화·장비·인장·메인 미션 + 공통 Goods) 통합 제공 +- **UI 클러스터**: `Lobby/CatTrade/` 11개 스크립트 (로비 최대 클러스터, 11 문서 §6-2 참조) +- **특수 로직**: + - 인장 룰렛(`SealRoulleteCard`·`SealSlotScroller`): 슬롯머신 연출 — 확률 기반 획득 + - 장비 구매 팝업(`CatTradeEquipmentBuyPopup`) + 기존 보유 장비 비교(`EquipmentTradeInvenCard`) + +### 4-3. 탐험 (Explore — 스테이지 선택) +- **구조**: 맵 → 스테이지 노드 선택 → 전투 진입 or 이벤트 +- **UI 클러스터**: `Lobby/Explore/` 7개 스크립트 (11 문서 §6-3) +- **저장 대상**: + - 현재 탐험 지도 ID·해제된 노드 + - 각 노드 클리어 여부·최고 스코어(3성 조건 달성) + - 탐험 보상 수령 이력 +- **P17 연계**: 스테이지별 ★ 조건 배치는 마스터 테이블(기획팀 `Stage`·`StarCondition`)에서 읽어 `ExploreUI_StageSelect`에 표시 + +### 4-4. 시즌 패스 +- **UI**: `Lobby/SeasonPass/SeasonPassUI.cs`·`SeasonPassCard.cs` +- **저장 대상**: 현재 시즌 ID·시즌 진행도 경험치·무료/유료 트랙별 수령 레벨 +- **서버 동기화 필요**: 시즌 교체·유료 트랙 결제 영수증 (PlayFab) + +### 4-5. 출석 +- **UI**: `Lobby/Attandance/AttandanceUI.cs`·`AttandanceCard.cs` +- **저장 대상**: 현재 월/회차·연속 출석일·수령 이력 +- **서버 시간 의존**: 날짜 조작 방지 위해 서버 시간 기준 (서버 지시서 §3 참고) + +## 5. 재화·경제 시스템 + +### 5-1. 재화 종류 +- 수상한 잡화점은 **복수 재화 체계** (골드·다이아·이벤트 화폐·카드 조각 등) +- 현 구조: `Info/*.Info` 내 변수 + 마스터 테이블 `Goods`·`Currency` 참조 추정 +- 표시 UI: `Lobby/LobbyTopUI.cs` + `Lobby/MoneyCard.cs` + 인게임 `IngameTopUI.cs` + +### 5-2. 획득 경로 +- 전투 클리어(`Ingame/Result/StageClearRewards`) +- 탐험 보상(`Lobby/Explore/ExploreRewardPopup`) +- 상점 구매(`Lobby/CatTrade/MerchantBuyPopup` 포함) +- 인앱 결제(`Info/InappInfo.cs` + PlayFab 영수증 검증) +- 광고 시청(`Info/ADInfo.cs`) +- 출석·시즌 패스 보상 + +### 5-3. 차기 프로젝트 흡수 (`NerdNavis.Economy`) +- `Goods` 범용 재화 모델 (타입·수량·최대치·오버플로 정책) — 01 설계안 §6-0 기정립 +- 인벤토리·획득 이벤트 훅(`EventBus` 연동) +- **본 프로젝트 특수 로직(재화 종류·상점 UX)은 흡수 불가** — 프레임워크는 "재화 모델 컨테이너"만 제공 + +## 6. 서버 연동 상태 (05 문서 연계) + +### 6-1. 현 서버 범위 (PlayFab) +- 세션/인증 → `ServerInfo.cs` +- 유저 데이터 로드/저장 → `ServerClass.cs` DTO 매핑 +- 스테이지 결과 기록 → `Save_StageResult` API (서버 지시서 §6 샘플) +- 인앱 영수증 검증 → PlayFab 영수증 검증 + +### 6-2. 서버 역할 경계 (서버 지시서 §5·§6) +- **클라 100% 책임**: 어뷰징 판정 (`is_abuse_flag`만 서버 전송, 경계값 보관·검증 안 함) +- **서버 100% 책임**: 시간 기반 판정(출석·시즌 만료) · 영수증 검증 · 로그 집계 +- **양쪽 책임**: 세이브 동기화 (클라 전송 + 서버 검증 최소 필터) + +## 7. 메타시스템 의존성 그래프 (핵심 흐름) + +``` +로그인 → Title_Mgr + ↓ (서버 인증 → PlayFab) +ServerInfo.Login → *.Info 다중 로드 + ↓ (마스터 테이블 로드, 10 문서) +DataCheckMgr·AddrResourceMgr + ↓ +LobbyUIManager.Initialize + ├→ LobbyTopUI (재화 표시) + ├→ MainMenu (영웅·카드·장비·미션 탭) + ├→ CatTrade (상점 진입) + ├→ Explore (탐험 진입) + │ ↓ (스테이지 선택) + │ IngameUIManager → 08 전투 FSM + │ ↓ (전투 종료) + │ StageClearRewards → *.Info 갱신 → ServerClass 전송 + │ ↓ + │ LobbyUIManager 복귀 + ├→ SeasonPass / Attandance (보상 수령) + └→ Option/WebView 등 +``` + +## 8. 기획 연동 포인트 (밸런싱·UX 변경 영향 범위) + +| 기획 변경 범주 | 영향 메타 모듈 | +|---------------|--------------| +| 재화 밸런스 (획득량·환율) | `Info/*.Info` 재화 필드 + `LobbyTopUI`·`MoneyCard`·`IngameTopUI` + 상점 UI 전체 | +| 성장 곡선 (장비 레벨·인장 등급) | `MainMenu_Equipment`·`CatTrade Seal 계열` + 마스터 테이블 `Equipment`·`Seal` | +| 시즌 패스 트랙 구성 | `SeasonPassUI`·`SeasonPassCard` + 서버 시즌 ID 갱신 | +| 출석 보상 트랙 | `AttandanceUI`·`AttandanceCard` + 서버 시간 의존 | +| 탐험 맵 구조 (노드·분기) | `ExploreUI_Map`·`ExploreStageCard` + 마스터 `ExploreMap`·`Stage` | +| 상점 가격·확률 (룰렛 등) | `CatTrade*` 전체 + 마스터 `Shop`·`Seal` | + +## 9. 프레임워크 흡수 계획 (Tier 2 설계 반영) + +| 프레임워크 모듈 | 수상한 잡화점 출처 | 흡수 수준 | +|--------------|------------------|---------| +| `NerdNavis.Save.ISaveProvider` | 현 `*.Info` + PlayFab 호출 패턴 | 🟡 신규 인터페이스 (기존 구조는 참고만) | +| `NerdNavis.Save.IMigration` | 없음 (부재 자체가 교훈) | 🔴 신규 설계 | +| `NerdNavis.Economy.Goods` | `MoneyCard`·재화 Info 필드 | 🟢 구조 계승, 네이밍만 변경 | +| `NerdNavis.Economy.Inventory` | 장비·인장·카드 목록 패턴 | 🟡 범용 컨테이너 추출 | +| `NerdNavis.Network.IReceiptVerifier` | `Info/InappInfo.cs` + PlayFab 영수증 | 🟡 Tier 3 편입 (서버팀 합류 시점) | + +## 10. 현 프로젝트에서의 차기 개선 안건 (차기 프로젝트 참고 자료) + +> 헌법 제1원칙 목표 2 원칙 B: 수상한 잡화점 인사이트를 차기 프로젝트 참고 자료로. + +1. `*.Info` 단일 클래스에 "런타임 상태 + 직렬화 대상 + 서버 통신 DTO"를 모두 담는 구조는 차기 프로젝트에서 **반드시 분리** (DTO + 상태 + 리포지토리 3계층) +2. 세이브 버전 관리 부재 → 차기 프로젝트 `IMigration` 훅 필수 +3. 서버/클라 동기화 타이밍이 호출부에 분산 → 차기 프로젝트는 **중앙 동기화 매니저** 도입 +4. 재화 종류가 하드코딩(필드 단위) → 차기 프로젝트는 `Goods` 범용 모델 + 타입 enum 기반 Dictionary 저장 + +## 11. 검증 방법 (P18 §검증) + +1. 각 Info/Server/UGUI 파일 경로는 실제 Unity 프로젝트에서 실존 확인 완료 (작성 시 `ls` + `find` 실측) +2. `find` 기반으로 `PlayFab`·`SaveData`·`UserData` 키워드를 검색한 결과는 `ServerClass.cs`·`ServerInfo.cs`·`Info/` 6개 파일로 집중됨 — 본 문서 §2 구조와 일치 +3. 마스터 테이블 이름은 10 문서(데이터 로딩) 및 서버 지시서 §6 샘플을 교차 참조하여 추정 (기획팀 테이블 정의 재확인 필요 표시) + +## 12. 기각안 (P24 §기각안) + +- **기각안 A: 각 Info 클래스의 필드별 세이브 대상 분류표 작성** — 토큰 비용 + 프레임워크 흡수 대비 정보량 과다. 구조 수준 분류로 대체. 필요 시 Phase 0-C에서 세부 감사 +- **기각안 B: 서버 API 전수 매핑** — 서버 지시서 v1.1/v1.2가 별도 SOT로 존재. 본 문서는 클라 메타 구조에 집중 +- **기각안 C: 메타시스템 보안 취약점 감사** — `05_서버연동_현황_v1.md` Critical 3건이 이미 추적 중(#2 보류). 본 문서는 중복 분석 안 함 + +## 부록 A. 변경 이력 +- **v1 (2026-04-17)**: 초판. 개발팀장 Phase 0-B 최종 완결 작업(B-5)으로 작성. Unity 프로젝트 `Assets/Script/` 전수 `ls` + 키워드 `find` 실측 기반. 서버 지시서 v1.1/v1.2·05 문서·09·10·11 문서 교차 참조. diff --git a/프로젝트/수상한잡화점/기획/밸런싱전략_v1.md b/프로젝트/수상한잡화점/기획/밸런싱전략_v1.md index 5d04b02..0e403d8 100644 --- a/프로젝트/수상한잡화점/기획/밸런싱전략_v1.md +++ b/프로젝트/수상한잡화점/기획/밸런싱전략_v1.md @@ -162,3 +162,14 @@ PC 스탯 → 몬스터 스탯 → 성장곡선 → 장비 → 인장 → 카드 2. **Phase 0의 앵커 기준(Stage1, PC6001, 장비/각성/인장 없음, G1만)**이 적절한가요? 3. **Phase 3의 기여도 목표(카드 +80~120%, 세트 장비 +60~80%, 각성 +40~60%…)**의 방향이 맞나요?** 4. **실행 로드맵의 주차별 진행이 소프트 론칭 일정에 현실적인가요?** + +--- + +## 변경 이력 (P16 산출물 추적성) + +> **표준 포맷 (2026-04-17 도입)**: 밸런싱 관련 문서 4종 공통 적용. 본 문서의 전략·Phase 정의·기여도 목표가 변경될 때마다 아래 테이블에 1행 append. +> **필드 설명**: 변경 필드 = 어떤 Phase·기여도 축·로드맵인가 / 이전값→이후값 = 구체 정의 / 재미 근거 = C7 원칙에 따른 재미 강화 축 / 관련 PD 지시# = `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md`의 해당 # + +| 일시 | 변경자 | 변경 필드 | 이전값→이후값 | 재미 근거 | 관련 PD 지시# | +|------|--------|---------|-------------|----------|-------------| +| 2026-04-17 | 기획팀장 | 변경 이력 테이블 섹션 신설 | - → 기존 상태 확정 | 산출물 추적성 확보 (P16), 전략 수정 시 결정 경로 추적 가능 | 본 작업 (팀장 재량 진행 승인) | diff --git a/프로젝트/수상한잡화점/기획/빌드_조건_충돌점검_v1.md b/프로젝트/수상한잡화점/기획/빌드_조건_충돌점검_v1.md index bfa3980..5fd3ce3 100644 --- a/프로젝트/수상한잡화점/기획/빌드_조건_충돌점검_v1.md +++ b/프로젝트/수상한잡화점/기획/빌드_조건_충돌점검_v1.md @@ -372,3 +372,14 @@ Headless C# 시뮬 추출 후 각 조합을 실측: *작성 완료: 2026-04-14* *상태: PD님 A안 "빌드 전수 점검" 이행 / Phase 3 재개 후 이슈 1·3 재논의 시 입력 자료* + +--- + +## 변경 이력 (P16 산출물 추적성) + +> **표준 포맷 (2026-04-17 도입)**: 밸런싱 관련 문서 4종 공통 적용. 본 문서의 충돌·배타 조합 판정·빌드 축이 변경될 때마다 아래 테이블에 1행 append. +> **필드 설명**: 변경 필드 = 어떤 조합·빌드 축·충돌 판정인가 / 이전값→이후값 = 구체 판정 변화 / 재미 근거 = C7 원칙에 따른 재미 강화 축 (빌드 다양성 등) / 관련 PD 지시# = `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md`의 해당 # + +| 일시 | 변경자 | 변경 필드 | 이전값→이후값 | 재미 근거 | 관련 PD 지시# | +|------|--------|---------|-------------|----------|-------------| +| 2026-04-17 | 기획팀장 | 변경 이력 테이블 섹션 신설 | - → 기존 상태 확정 | 산출물 추적성 확보 (P16), 배타 조합(P17) 변경 시 근거 추적 가능 | 본 작업 (팀장 재량 진행 승인) | diff --git a/프로젝트/수상한잡화점/기획/스테이지난이도곡선_v1.md b/프로젝트/수상한잡화점/기획/스테이지난이도곡선_v1.md index 8a25065..03203e9 100644 --- a/프로젝트/수상한잡화점/기획/스테이지난이도곡선_v1.md +++ b/프로젝트/수상한잡화점/기획/스테이지난이도곡선_v1.md @@ -307,3 +307,14 @@ Stage: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 *분석 완료: 2026-04-13* *분석자: Claude (데이터 자동 추출 및 계산)* + +--- + +## 변경 이력 (P16 산출물 추적성) + +> **표준 포맷 (2026-04-17 도입)**: 밸런싱 관련 문서 4종 공통 적용. 본 문서의 수치·구간·난이도 평가가 변경될 때마다 아래 테이블에 1행 append. +> **필드 설명**: 변경 필드 = 어떤 구간·스테이지·수치인가 / 이전값→이후값 = 구체 수치 / 재미 근거 = C7 원칙에 따른 재미 강화 축 / 관련 PD 지시# = `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md`의 해당 # + +| 일시 | 변경자 | 변경 필드 | 이전값→이후값 | 재미 근거 | 관련 PD 지시# | +|------|--------|---------|-------------|----------|-------------| +| 2026-04-17 | 기획팀장 | 변경 이력 테이블 섹션 신설 | - → 기존 상태 확정 | 산출물 추적성 확보 (P16), 차기 밸런스 변경 시 근거 비교 가능 | 본 작업 (팀장 재량 진행 승인) | diff --git a/프로젝트/수상한잡화점/기획/전체테이블감사_v1.md b/프로젝트/수상한잡화점/기획/전체테이블감사_v1.md index 68fec40..aa0ebcd 100644 --- a/프로젝트/수상한잡화점/기획/전체테이블감사_v1.md +++ b/프로젝트/수상한잡화점/기획/전체테이블감사_v1.md @@ -581,3 +581,14 @@ D:/NerdNavis/FilGoodBandits/DeckBuilding/Assets/ResWork/Table/Export/ ItemList.json - 아이템/재화 (225) RewardRandomBag.json - 보상 풀 (1342) ``` + +--- + +## 변경 이력 (P16 산출물 추적성) + +> **표준 포맷 (2026-04-17 도입)**: 밸런싱 관련 문서 4종 공통 적용. 본 문서의 테이블 감사 결과·Export 목록·스탯 범위 판정이 변경될 때마다 아래 테이블에 1행 append. +> **필드 설명**: 변경 필드 = 어떤 테이블·감사 항목인가 / 이전값→이후값 = 구체 감사 결과 변화 / 재미 근거 = C7 원칙에 따른 재미 강화 축 / 관련 PD 지시# = `공유/PD_지시_트래킹/기획팀_PD_지시_로그.md`의 해당 # + +| 일시 | 변경자 | 변경 필드 | 이전값→이후값 | 재미 근거 | 관련 PD 지시# | +|------|--------|---------|-------------|----------|-------------| +| 2026-04-17 | 기획팀장 | 변경 이력 테이블 섹션 신설 | - → 기존 상태 확정 | 산출물 추적성 확보 (P16), 테이블 구조 변경 추적 가능 | 본 작업 (팀장 재량 진행 승인) |