diff --git a/.playwright-mcp/console-2026-08-21T10-06-16-202Z.log b/.playwright-mcp/console-2026-08-21T10-06-16-202Z.log new file mode 100644 index 0000000..68b3eeb --- /dev/null +++ b/.playwright-mcp/console-2026-08-21T10-06-16-202Z.log @@ -0,0 +1 @@ +[ 1019ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0 diff --git a/.playwright-mcp/console-2026-08-21T10-06-40-806Z.log b/.playwright-mcp/console-2026-08-21T10-06-40-806Z.log new file mode 100644 index 0000000..720ea14 --- /dev/null +++ b/.playwright-mcp/console-2026-08-21T10-06-40-806Z.log @@ -0,0 +1 @@ +[ 39361ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/cases/b09f168b-b205-478f-b60f-83872d1b4d01:0 diff --git a/.playwright-mcp/console-2026-08-21T10-10-58-031Z.log b/.playwright-mcp/console-2026-08-21T10-10-58-031Z.log new file mode 100644 index 0000000..7168a49 --- /dev/null +++ b/.playwright-mcp/console-2026-08-21T10-10-58-031Z.log @@ -0,0 +1 @@ +[ 4651ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/cases/b09f168b-b205-478f-b60f-83872d1b4d01:0 diff --git a/.playwright-mcp/page-2026-08-21T10-06-17-092Z.yml b/.playwright-mcp/page-2026-08-21T10-06-17-092Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/.playwright-mcp/page-2026-08-21T10-06-25-904Z.yml b/.playwright-mcp/page-2026-08-21T10-06-25-904Z.yml new file mode 100644 index 0000000..26923fc --- /dev/null +++ b/.playwright-mcp/page-2026-08-21T10-06-25-904Z.yml @@ -0,0 +1,16 @@ +- generic [ref=f1e3]: + - banner [ref=f1e4]: + - generic [ref=f1e5]: prod + - main [ref=f1e6]: + - heading "Sign in to your account" [level=1] [ref=f1e8] + - generic [ref=f1e12]: + - generic [ref=f1e13]: + - generic [ref=f1e14]: Username or email + - textbox "Username or email" [active] [ref=f1e17] + - generic [ref=f1e18]: + - generic [ref=f1e19]: Password + - generic [ref=f1e21]: + - textbox "Password" [ref=f1e24] + - button "Show password" [ref=f1e26] [cursor=pointer]: + - generic [ref=f1e27]: + - button "Sign In" [ref=f1e30] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-21T10-06-35-904Z.yml b/.playwright-mcp/page-2026-08-21T10-06-35-904Z.yml new file mode 100644 index 0000000..5e6f206 --- /dev/null +++ b/.playwright-mcp/page-2026-08-21T10-06-35-904Z.yml @@ -0,0 +1,74 @@ +- generic [ref=f2e3]: + - link "본문으로 건너뛰기" [ref=f2e4] [cursor=pointer]: + - /url: "#main-content" + - banner [ref=f2e5]: + - generic [ref=f2e6]: + - link "TechLog 홈" [ref=f2e8] [cursor=pointer]: + - /url: / + - text: TechLog + - generic [ref=f2e9]: + - button "TechLog 검색 열기" [ref=f2e11] [cursor=pointer]: 검색 + - group [ref=f2e12]: + - generic "메뉴" [ref=f2e13] [cursor=pointer] + - main [ref=f2e14]: + - region [ref=f2e15]: + - heading "TechLog" [level=1] [ref=f2e16] + - paragraph [ref=f2e17]: 문제를 재현하고 검증해 운영 가능한 설계로 연결합니다. + - region [ref=f2e18]: + - generic [ref=f2e19]: + - generic [ref=f2e20]: + - paragraph [ref=f2e21]: Index + - heading "최근 기록" [level=2] [ref=f2e22] + - link "모든 기록 탐색" [ref=f2e23] [cursor=pointer]: + - /url: /explore + - list [ref=f2e24]: + - listitem [ref=f2e25]: + - link "RELEASE 2026.08.21 첫 공개 공개 사이트와 Studio 작성 흐름을 처음으로 실제 서버에 올렸습니다. TechLog · TechLog" [ref=f2e26] [cursor=pointer]: + - /url: /releases/0.1.0 + - generic [ref=f2e27]: + - generic [ref=f2e28]: RELEASE + - time [ref=f2e29]: 2026.08.21 + - generic [ref=f2e30]: + - heading "첫 공개" [level=3] [ref=f2e31] + - paragraph [ref=f2e32]: 공개 사이트와 Studio 작성 흐름을 처음으로 실제 서버에 올렸습니다. + - paragraph [ref=f2e33]: TechLog · TechLog + - generic [ref=f2e34]: ↗ + - region [ref=f2e35]: + - generic [ref=f2e37]: + - paragraph [ref=f2e38]: Explore + - heading "어떤 맥락으로 읽을까요?" [level=2] [ref=f2e39] + - list [ref=f2e40]: + - listitem [ref=f2e41]: + - link "문제를 따라가며 검증 과정을 읽습니다 Case" [ref=f2e42] [cursor=pointer]: + - /url: /explore/cases + - generic [ref=f2e43]: 문제를 따라가며 검증 과정을 읽습니다 + - strong [ref=f2e44]: Case + - generic [ref=f2e45]: ↗ + - listitem [ref=f2e46]: + - link "다시 찾을 수 있는 기술 기준을 확인합니다 Reference" [ref=f2e47] [cursor=pointer]: + - /url: /explore/references + - generic [ref=f2e48]: 다시 찾을 수 있는 기술 기준을 확인합니다 + - strong [ref=f2e49]: Reference + - generic [ref=f2e50]: ↗ + - listitem [ref=f2e51]: + - link "아직 끝나지 않은 판단과 다음 검증을 봅니다 OpenQuestion" [ref=f2e52] [cursor=pointer]: + - /url: /explore/questions + - generic [ref=f2e53]: 아직 끝나지 않은 판단과 다음 검증을 봅니다 + - strong [ref=f2e54]: OpenQuestion + - generic [ref=f2e55]: ↗ + - listitem [ref=f2e56]: + - link "여러 기록을 하나의 시스템 맥락에서 연결합니다 Project" [ref=f2e57] [cursor=pointer]: + - /url: /projects + - generic [ref=f2e58]: 여러 기록을 하나의 시스템 맥락에서 연결합니다 + - strong [ref=f2e59]: Project + - generic [ref=f2e60]: ↗ + - contentinfo [ref=f2e61]: + - generic [ref=f2e62]: + - generic [ref=f2e63]: + - paragraph [ref=f2e64]: 동현 + - paragraph [ref=f2e65]: 문제를 재현하고 검증해 운영 가능한 설계로 연결합니다. + - generic [ref=f2e66]: + - link "프로필" [ref=f2e67] [cursor=pointer]: + - /url: /profile + - link "변경 기록" [ref=f2e68] [cursor=pointer]: + - /url: /releases \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-21T10-06-40-962Z.yml b/.playwright-mcp/page-2026-08-21T10-06-40-962Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/.playwright-mcp/page-2026-08-21T10-07-21-058Z.yml b/.playwright-mcp/page-2026-08-21T10-07-21-058Z.yml new file mode 100644 index 0000000..287cc78 --- /dev/null +++ b/.playwright-mcp/page-2026-08-21T10-07-21-058Z.yml @@ -0,0 +1,100 @@ +- generic [ref=f3e22]: + - link "본문으로 건너뛰기" [ref=f3e23] [cursor=pointer]: + - /url: "#main-content" + - banner [ref=f3e24]: + - generic [ref=f3e25]: + - link "TechLog Studio" [ref=f3e26] [cursor=pointer]: + - /url: /studio + - text: TechLog + - generic [ref=f3e27]: Studio + - navigation "Studio 주 탐색" [ref=f3e29]: + - link "작업본" [ref=f3e30] [cursor=pointer]: + - /url: /studio/documents + - link "게시 기록" [ref=f3e31] [cursor=pointer]: + - /url: /studio/publications + - link "새 문서" [ref=f3e32] [cursor=pointer]: + - /url: /studio/documents/new + - link "주제·프로젝트" [ref=f3e33] [cursor=pointer]: + - /url: /studio/taxonomy + - link "릴리즈" [ref=f3e34] [cursor=pointer]: + - /url: /studio/releases + - link "공개 사이트 보기" [ref=f3e35] [cursor=pointer]: + - /url: / + - button "로그아웃" [ref=f3e36] + - main [ref=f3e37]: + - generic [ref=f3e38]: + - generic [ref=f3e39]: + - generic [ref=f3e40]: + - paragraph [ref=f3e41]: WORKING COPIES + - heading "작업본" [level=1] [ref=f3e42] + - paragraph [ref=f3e43]: 세션에 있는 Case, Reference, Question, Decision을 찾고 다음 작업으로 이동합니다. + - link "새 문서" [ref=f3e44] [cursor=pointer]: + - /url: /studio/documents/new + - region "작업본 검색과 필터" [ref=f3e45]: + - search [ref=f3e46]: + - generic [ref=f3e47]: 검색 + - generic [ref=f3e48]: + - searchbox "검색" [ref=f3e49] + - button "검색" [ref=f3e50] + - generic [ref=f3e51]: + - text: 종류 + - combobox "종류" [ref=f3e52]: + - option "전체" [selected] + - option "Case" + - option "Reference" + - option "Question" + - option "Decision" + - generic [ref=f3e53]: + - text: 상태 + - combobox "상태" [ref=f3e54]: + - option "전체" [selected] + - option "게시 전" + - option "게시 중" + - option "게시 취소" + - paragraph [ref=f3e55]: + - generic [ref=f3e56]: 2개 표시 중 + - alert [ref=f3e57]: 삭제하지 못했습니다. 게시 중이거나, 이 기록을 참조하는 곳이 있거나, 다른 곳에서 먼저 수정되었을 수 있습니다. + - generic [ref=f3e58]: + - article [ref=f3e1]: + - paragraph [ref=f3e2]: Case + - generic [ref=f3e3]: + - heading [level=2] [ref=f3e4]: + - link "게시 흐름 확인" [ref=f3e5] [cursor=pointer]: + - /url: /studio/documents/b09f168b-b205-478f-b60f-83872d1b4d01/edit + - paragraph [ref=f3e6]: 프로젝트 미지정 + - generic [ref=f3e7]: + - generic [ref=f3e8]: + - term [ref=f3e9]: 상태 + - definition [ref=f3e10]: 게시 취소 + - generic [ref=f3e11]: + - term [ref=f3e12]: 다음 + - definition [ref=f3e13]: + - link "검증하기" [ref=f3e14] [cursor=pointer]: + - /url: /studio/documents/b09f168b-b205-478f-b60f-83872d1b4d01/validation + - generic [ref=f3e15]: + - term [ref=f3e16]: 수정 + - definition [ref=f3e17]: + - time [ref=f3e18]: 2026. 8. 21. + - button "삭제" [ref=f3e19] + - article [ref=f3e59]: + - paragraph [ref=f3e60]: Case + - generic [ref=f3e61]: + - heading [level=2] [ref=f3e62]: + - link "브라우저에서 토큰을 어디까지 다뤄야 하는가..?" [ref=f3e63] [cursor=pointer]: + - /url: /studio/documents/f363edc8-2c13-4995-acda-934237034a85/edit + - paragraph [ref=f3e64]: 프로젝트 미지정 + - generic [ref=f3e65]: + - generic [ref=f3e66]: + - term [ref=f3e67]: 상태 + - definition [ref=f3e68]: 게시 전 + - generic [ref=f3e69]: + - term [ref=f3e70]: 다음 + - definition [ref=f3e71]: + - link "검증하기" [ref=f3e72] [cursor=pointer]: + - /url: /studio/documents/f363edc8-2c13-4995-acda-934237034a85/validation + - generic [ref=f3e73]: + - term [ref=f3e74]: 수정 + - definition [ref=f3e75]: + - time [ref=f3e76]: 2026. 8. 21. + - button "삭제" [ref=f3e77] + - paragraph [ref=f3e78] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-21T10-08-20-113Z.yml b/.playwright-mcp/page-2026-08-21T10-08-20-113Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/.playwright-mcp/page-2026-08-21T10-08-24-554Z.yml b/.playwright-mcp/page-2026-08-21T10-08-24-554Z.yml new file mode 100644 index 0000000..513732f --- /dev/null +++ b/.playwright-mcp/page-2026-08-21T10-08-24-554Z.yml @@ -0,0 +1,55 @@ +- generic [ref=f4e3]: + - link "본문으로 건너뛰기" [ref=f4e4] [cursor=pointer]: + - /url: "#main-content" + - banner [ref=f4e5]: + - generic [ref=f4e6]: + - link "TechLog Studio" [ref=f4e7] [cursor=pointer]: + - /url: /studio + - text: TechLog + - generic [ref=f4e8]: Studio + - navigation "Studio 주 탐색" [ref=f4e10]: + - link "작업본" [ref=f4e11] [cursor=pointer]: + - /url: /studio/documents + - link "게시 기록" [ref=f4e12] [cursor=pointer]: + - /url: /studio/publications + - link "새 문서" [ref=f4e13] [cursor=pointer]: + - /url: /studio/documents/new + - link "주제·프로젝트" [ref=f4e14] [cursor=pointer]: + - /url: /studio/taxonomy + - link "릴리즈" [ref=f4e15] [cursor=pointer]: + - /url: /studio/releases + - link "공개 사이트 보기" [ref=f4e16] [cursor=pointer]: + - /url: / + - button "로그아웃" [ref=f4e17] + - main [ref=f4e18]: + - generic [ref=f4e19]: + - generic [ref=f4e20]: + - paragraph [ref=f4e21]: NEW WORKING COPY + - heading "새 문서" [level=1] [ref=f4e22] + - paragraph [ref=f4e23]: 목적에 맞는 기록 종류를 선택하면 빈 작업본을 만들고 바로 편집을 시작합니다. + - generic [ref=f4e24]: + - group "문서 종류" [ref=f4e25]: + - generic [ref=f4e27] [cursor=pointer]: + - radio "Case 문제를 재현하고 검증한 결론을 기록합니다. 문제 · 결론 · 환경 · 재현 · 본문" [checked] [active] [ref=f4e28] + - strong [ref=f4e29]: Case + - generic [ref=f4e30]: 문제를 재현하고 검증한 결론을 기록합니다. + - generic [ref=f4e31]: 문제 · 결론 · 환경 · 재현 · 본문 + - generic [ref=f4e32] [cursor=pointer]: + - radio "Reference 반복해서 적용할 기술 기준을 정리합니다. 목적 · 규칙 · 적용 조건 · 예외 · 예시" [ref=f4e33] + - strong [ref=f4e34]: Reference + - generic [ref=f4e35]: 반복해서 적용할 기술 기준을 정리합니다. + - generic [ref=f4e36]: 목적 · 규칙 · 적용 조건 · 예외 · 예시 + - generic [ref=f4e37] [cursor=pointer]: + - radio "Question 아직 닫히지 않은 판단과 다음 검증을 관리합니다. 상태 · 사실 · 가정 · 미지수 · 선택지" [ref=f4e38] + - strong [ref=f4e39]: Question + - generic [ref=f4e40]: 아직 닫히지 않은 판단과 다음 검증을 관리합니다. + - generic [ref=f4e41]: 상태 · 사실 · 가정 · 미지수 · 선택지 + - generic [ref=f4e42] [cursor=pointer]: + - radio "Decision 프로젝트가 선택한 방향과 그 근거·영향을 기록합니다. 상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거" [ref=f4e43] + - strong [ref=f4e44]: Decision + - generic [ref=f4e45]: 프로젝트가 선택한 방향과 그 근거·영향을 기록합니다. + - generic [ref=f4e46]: 상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거 + - generic [ref=f4e47]: + - button "작업본 만들기" [ref=f4e48] + - paragraph [ref=f4e49]: 이 화면의 작업본은 현재 Studio 세션에서만 유지됩니다. + - paragraph [ref=f4e50] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-21T10-08-30-303Z.yml b/.playwright-mcp/page-2026-08-21T10-08-30-303Z.yml new file mode 100644 index 0000000..676f186 --- /dev/null +++ b/.playwright-mcp/page-2026-08-21T10-08-30-303Z.yml @@ -0,0 +1,143 @@ +- generic [ref=f4e3]: + - link "본문으로 건너뛰기" [ref=f4e4] [cursor=pointer]: + - /url: "#main-content" + - banner [ref=f4e5]: + - generic [ref=f4e6]: + - link "TechLog Studio" [ref=f4e7] [cursor=pointer]: + - /url: /studio + - text: TechLog + - generic [ref=f4e8]: Studio + - navigation "Studio 주 탐색" [ref=f4e10]: + - link "작업본" [ref=f4e11] [cursor=pointer]: + - /url: /studio/documents + - link "게시 기록" [ref=f4e12] [cursor=pointer]: + - /url: /studio/publications + - link "새 문서" [ref=f4e13] [cursor=pointer]: + - /url: /studio/documents/new + - link "주제·프로젝트" [ref=f4e14] [cursor=pointer]: + - /url: /studio/taxonomy + - link "릴리즈" [ref=f4e15] [cursor=pointer]: + - /url: /studio/releases + - link "공개 사이트 보기" [ref=f4e16] [cursor=pointer]: + - /url: / + - button "로그아웃" [ref=f4e17] + - main [ref=f4e18]: + - generic [ref=f4e51]: + - tablist "문서 편집 화면" [ref=f4e52]: + - tab "편집" [selected] [ref=f4e53] + - tab "즉시 미리보기" [ref=f4e54] + - generic [ref=f4e55]: + - tabpanel "편집" [ref=f4e57]: + - generic [ref=f4e58]: + - paragraph [ref=f4e59]: CASE · VERSION 1 + - heading "문서 편집" [level=1] [ref=f4e60] + - paragraph [ref=f4e61]: 제목 없는 작업본 + - region [ref=f4e62]: + - generic [ref=f4e63]: + - paragraph [ref=f4e64]: DOCUMENT + - heading "기본 정보" [level=2] [ref=f4e65] + - generic [ref=f4e66]: + - generic [ref=f4e67]: + - generic [ref=f4e68]: 제목 + - textbox "제목" [ref=f4e69] + - generic [ref=f4e70]: + - generic [ref=f4e71]: slug + - textbox "slug" [ref=f4e72]: + - /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈) + - generic [ref=f4e73]: + - generic [ref=f4e74]: 요약 + - textbox "요약" [ref=f4e75] + - generic [ref=f4e76]: + - generic [ref=f4e77]: Topic + - combobox "Topic" [ref=f4e78]: + - option "선택하지 않음" [selected] + - option "OAuth/OIDC 인증 경계" + - generic [ref=f4e79]: + - generic [ref=f4e80]: Project + - combobox "Project" [ref=f4e81]: + - option "미지정" [selected] + - option "Backend Clean Architecture" + - option "KeyCloak Patterns" + - option "Liner N + 1문제" + - group "관계" [ref=f4e82]: + - paragraph [ref=f4e84]: 연결한 공개 기록이 없습니다. + - button "관계 추가" [ref=f4e85] + - region [ref=f4e86]: + - generic [ref=f4e87]: + - paragraph [ref=f4e88]: CASE + - heading "문제와 검증" [level=2] [ref=f4e89] + - generic [ref=f4e90]: + - generic [ref=f4e91]: + - generic [ref=f4e92]: 문제 + - textbox "문제" [ref=f4e93] + - generic [ref=f4e94]: + - generic [ref=f4e95]: 결론 + - textbox "결론" [ref=f4e96] + - generic [ref=f4e97]: + - generic [ref=f4e98]: 검증 환경 + - textbox "검증 환경" [ref=f4e99] + - generic [ref=f4e100]: + - generic [ref=f4e101]: 재현 조건 + - textbox "재현 조건" [ref=f4e102] + - generic [ref=f4e103]: + - generic [ref=f4e104]: 마지막 검증일 + - textbox "마지막 검증일" [ref=f4e105] + - generic [ref=f4e106]: + - generic [ref=f4e107]: 본문 Markdown + - textbox "본문 Markdown" [ref=f4e108] + - generic [ref=f4e109]: + - paragraph [ref=f4e110]: EVIDENCE + - heading "본문에 Asset 삽입" [level=3] [ref=f4e111] + - paragraph [ref=f4e112]: 목록에서 선택하면 본문 커서 위치에 evidence 구문을 삽입합니다. READY 상태의 Asset만 선택할 수 있습니다. + - generic [ref=f4e113]: + - generic [ref=f4e114]: + - generic [ref=f4e115]: 업로드 종류 + - combobox "업로드 종류" [ref=f4e116]: + - option "이미지" [selected] + - option "다이어그램" + - option "첨부파일" + - button "Asset 업로드" [ref=f4e117] + - generic [ref=f4e118]: + - search [ref=f4e119]: + - generic [ref=f4e120]: Asset 검색 + - generic [ref=f4e121]: + - searchbox "Asset 검색" [ref=f4e122] + - button "검색" [ref=f4e123] + - generic [ref=f4e124]: + - checkbox "삽입할 때 크게 보기 허용" [checked] [ref=f4e125] + - generic [ref=f4e126]: 삽입할 때 크게 보기 허용 + - status [ref=f4e127]: 삽입할 수 있는 Asset 1개 + - list [ref=f4e128]: + - listitem [ref=f4e129]: + - button "screenshot-from-2026-08-21-18-04-49-72f1f9c6" [ref=f4e130] + - button "삭제" [ref=f4e131] + - complementary [ref=f4e132]: + - paragraph [ref=f4e133]: WORKING COPY + - heading "작업 상태" [level=2] [ref=f4e134] + - status "편집 상태" [ref=f4e135]: 저장됨 + - generic [ref=f4e136]: + - generic [ref=f4e137]: + - term [ref=f4e138]: 저장 버전 + - definition [ref=f4e139]: "1" + - generic [ref=f4e140]: + - term [ref=f4e141]: 종류 + - definition [ref=f4e142]: CASE + - button "저장" [disabled] [ref=f4e143] + - navigation "게시까지의 단계" [ref=f4e144]: + - list [ref=f4e145]: + - listitem [ref=f4e146]: + - generic [ref=f4e147]: "1" + - link "검증" [ref=f4e148] [cursor=pointer]: + - /url: /studio/documents/969cc2c7-be6e-42ca-924a-652c63db9784/validation + - listitem [ref=f4e149]: + - text: → + - generic [ref=f4e150]: "2" + - link "미리보기" [ref=f4e151] [cursor=pointer]: + - /url: /studio/documents/969cc2c7-be6e-42ca-924a-652c63db9784/preview + - listitem [ref=f4e152]: + - text: → + - generic [ref=f4e153]: "3" + - link "게시" [ref=f4e154] [cursor=pointer]: + - /url: /studio/documents/969cc2c7-be6e-42ca-924a-652c63db9784/publish + - paragraph [ref=f4e155]: 불완전한 초안도 저장할 수 있습니다. 게시 가능 여부는 이후 검증 단계에서 확인합니다. + - paragraph [ref=f4e50]: Case 작업본을 만들었습니다. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-21T10-08-35-304Z.yml b/.playwright-mcp/page-2026-08-21T10-08-35-304Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/.playwright-mcp/page-2026-08-21T10-08-44-012Z.yml b/.playwright-mcp/page-2026-08-21T10-08-44-012Z.yml new file mode 100644 index 0000000..198c4e2 --- /dev/null +++ b/.playwright-mcp/page-2026-08-21T10-08-44-012Z.yml @@ -0,0 +1,99 @@ +- generic [ref=f5e22]: + - link "본문으로 건너뛰기" [ref=f5e23] [cursor=pointer]: + - /url: "#main-content" + - banner [ref=f5e24]: + - generic [ref=f5e25]: + - link "TechLog Studio" [ref=f5e26] [cursor=pointer]: + - /url: /studio + - text: TechLog + - generic [ref=f5e27]: Studio + - navigation "Studio 주 탐색" [ref=f5e29]: + - link "작업본" [ref=f5e30] [cursor=pointer]: + - /url: /studio/documents + - link "게시 기록" [ref=f5e31] [cursor=pointer]: + - /url: /studio/publications + - link "새 문서" [ref=f5e32] [cursor=pointer]: + - /url: /studio/documents/new + - link "주제·프로젝트" [ref=f5e33] [cursor=pointer]: + - /url: /studio/taxonomy + - link "릴리즈" [ref=f5e34] [cursor=pointer]: + - /url: /studio/releases + - link "공개 사이트 보기" [ref=f5e35] [cursor=pointer]: + - /url: / + - button "로그아웃" [ref=f5e36] + - main [ref=f5e37]: + - generic [ref=f5e38]: + - generic [ref=f5e39]: + - generic [ref=f5e40]: + - paragraph [ref=f5e41]: WORKING COPIES + - heading "작업본" [level=1] [ref=f5e42] + - paragraph [ref=f5e43]: 세션에 있는 Case, Reference, Question, Decision을 찾고 다음 작업으로 이동합니다. + - link "새 문서" [ref=f5e44] [cursor=pointer]: + - /url: /studio/documents/new + - region "작업본 검색과 필터" [ref=f5e45]: + - search [ref=f5e46]: + - generic [ref=f5e47]: 검색 + - generic [ref=f5e48]: + - searchbox "검색" [ref=f5e49] + - button "검색" [ref=f5e50] + - generic [ref=f5e51]: + - text: 종류 + - combobox "종류" [ref=f5e52]: + - option "전체" [selected] + - option "Case" + - option "Reference" + - option "Question" + - option "Decision" + - generic [ref=f5e53]: + - text: 상태 + - combobox "상태" [ref=f5e54]: + - option "전체" [selected] + - option "게시 전" + - option "게시 중" + - option "게시 취소" + - paragraph [ref=f5e55]: + - generic [ref=f5e56]: 2개 표시 중 + - generic [ref=f5e57]: + - article [ref=f5e58]: + - paragraph [ref=f5e59]: Case + - generic [ref=f5e60]: + - heading [level=2] [ref=f5e61]: + - link "게시 흐름 확인" [ref=f5e62] [cursor=pointer]: + - /url: /studio/documents/b09f168b-b205-478f-b60f-83872d1b4d01/edit + - paragraph [ref=f5e63]: 프로젝트 미지정 + - generic [ref=f5e64]: + - generic [ref=f5e65]: + - term [ref=f5e66]: 상태 + - definition [ref=f5e67]: 게시 취소 + - generic [ref=f5e68]: + - term [ref=f5e69]: 다음 + - definition [ref=f5e70]: + - link "검증하기" [ref=f5e71] [cursor=pointer]: + - /url: /studio/documents/b09f168b-b205-478f-b60f-83872d1b4d01/validation + - generic [ref=f5e72]: + - term [ref=f5e73]: 수정 + - definition [ref=f5e74]: + - time [ref=f5e75]: 2026. 8. 21. + - button "삭제" [ref=f5e76] + - article [ref=f5e77]: + - paragraph [ref=f5e78]: Case + - generic [ref=f5e79]: + - heading [level=2] [ref=f5e80]: + - link "브라우저에서 토큰을 어디까지 다뤄야 하는가..?" [ref=f5e81] [cursor=pointer]: + - /url: /studio/documents/f363edc8-2c13-4995-acda-934237034a85/edit + - paragraph [ref=f5e82]: 프로젝트 미지정 + - generic [ref=f5e83]: + - generic [ref=f5e84]: + - term [ref=f5e85]: 상태 + - definition [ref=f5e86]: 게시 전 + - generic [ref=f5e87]: + - term [ref=f5e88]: 다음 + - definition [ref=f5e89]: + - link "검증하기" [ref=f5e90] [cursor=pointer]: + - /url: /studio/documents/f363edc8-2c13-4995-acda-934237034a85/validation + - generic [ref=f5e91]: + - term [ref=f5e92]: 수정 + - definition [ref=f5e93]: + - time [ref=f5e94]: 2026. 8. 21. + - button "삭제" [ref=f5e95] + - paragraph [ref=f5e96]: 작업본 제목 없음 을(를) 삭제했습니다. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-21T10-09-11-287Z.yml b/.playwright-mcp/page-2026-08-21T10-09-11-287Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/.playwright-mcp/page-2026-08-21T10-09-16-578Z.yml b/.playwright-mcp/page-2026-08-21T10-09-16-578Z.yml new file mode 100644 index 0000000..d4f7965 --- /dev/null +++ b/.playwright-mcp/page-2026-08-21T10-09-16-578Z.yml @@ -0,0 +1,55 @@ +- generic [ref=f6e3]: + - link "본문으로 건너뛰기" [ref=f6e4] [cursor=pointer]: + - /url: "#main-content" + - banner [ref=f6e5]: + - generic [ref=f6e6]: + - link "TechLog Studio" [ref=f6e7] [cursor=pointer]: + - /url: /studio + - text: TechLog + - generic [ref=f6e8]: Studio + - navigation "Studio 주 탐색" [ref=f6e10]: + - link "작업본" [ref=f6e11] [cursor=pointer]: + - /url: /studio/documents + - link "게시 기록" [ref=f6e12] [cursor=pointer]: + - /url: /studio/publications + - link "새 문서" [ref=f6e13] [cursor=pointer]: + - /url: /studio/documents/new + - link "주제·프로젝트" [ref=f6e14] [cursor=pointer]: + - /url: /studio/taxonomy + - link "릴리즈" [ref=f6e15] [cursor=pointer]: + - /url: /studio/releases + - link "공개 사이트 보기" [ref=f6e16] [cursor=pointer]: + - /url: / + - button "로그아웃" [ref=f6e17] + - main [ref=f6e18]: + - generic [ref=f6e19]: + - generic [ref=f6e20]: + - paragraph [ref=f6e21]: NEW WORKING COPY + - heading "새 문서" [level=1] [ref=f6e22] + - paragraph [ref=f6e23]: 목적에 맞는 기록 종류를 선택하면 빈 작업본을 만들고 바로 편집을 시작합니다. + - generic [ref=f6e24]: + - group "문서 종류" [ref=f6e25]: + - generic [ref=f6e27] [cursor=pointer]: + - radio "Case 문제를 재현하고 검증한 결론을 기록합니다. 문제 · 결론 · 환경 · 재현 · 본문" [ref=f6e28] + - strong [ref=f6e29]: Case + - generic [ref=f6e30]: 문제를 재현하고 검증한 결론을 기록합니다. + - generic [ref=f6e31]: 문제 · 결론 · 환경 · 재현 · 본문 + - generic [ref=f6e32] [cursor=pointer]: + - radio "Reference 반복해서 적용할 기술 기준을 정리합니다. 목적 · 규칙 · 적용 조건 · 예외 · 예시" [ref=f6e33] + - strong [ref=f6e34]: Reference + - generic [ref=f6e35]: 반복해서 적용할 기술 기준을 정리합니다. + - generic [ref=f6e36]: 목적 · 규칙 · 적용 조건 · 예외 · 예시 + - generic [ref=f6e37] [cursor=pointer]: + - radio "Question 아직 닫히지 않은 판단과 다음 검증을 관리합니다. 상태 · 사실 · 가정 · 미지수 · 선택지" [checked] [active] [ref=f6e38] + - strong [ref=f6e39]: Question + - generic [ref=f6e40]: 아직 닫히지 않은 판단과 다음 검증을 관리합니다. + - generic [ref=f6e41]: 상태 · 사실 · 가정 · 미지수 · 선택지 + - generic [ref=f6e42] [cursor=pointer]: + - radio "Decision 프로젝트가 선택한 방향과 그 근거·영향을 기록합니다. 상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거" [ref=f6e43] + - strong [ref=f6e44]: Decision + - generic [ref=f6e45]: 프로젝트가 선택한 방향과 그 근거·영향을 기록합니다. + - generic [ref=f6e46]: 상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거 + - generic [ref=f6e47]: + - button "작업본 만들기" [ref=f6e48] + - paragraph [ref=f6e49]: 이 화면의 작업본은 현재 Studio 세션에서만 유지됩니다. + - paragraph [ref=f6e50] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-21T10-09-26-317Z.yml b/.playwright-mcp/page-2026-08-21T10-09-26-317Z.yml new file mode 100644 index 0000000..6f0043c --- /dev/null +++ b/.playwright-mcp/page-2026-08-21T10-09-26-317Z.yml @@ -0,0 +1,122 @@ +- generic [ref=f6e3]: + - link "본문으로 건너뛰기" [ref=f6e4] [cursor=pointer]: + - /url: "#main-content" + - banner [ref=f6e5]: + - generic [ref=f6e6]: + - link "TechLog Studio" [ref=f6e7] [cursor=pointer]: + - /url: /studio + - text: TechLog + - generic [ref=f6e8]: Studio + - navigation "Studio 주 탐색" [ref=f6e10]: + - link "작업본" [ref=f6e11] [cursor=pointer]: + - /url: /studio/documents + - link "게시 기록" [ref=f6e12] [cursor=pointer]: + - /url: /studio/publications + - link "새 문서" [ref=f6e13] [cursor=pointer]: + - /url: /studio/documents/new + - link "주제·프로젝트" [ref=f6e14] [cursor=pointer]: + - /url: /studio/taxonomy + - link "릴리즈" [ref=f6e15] [cursor=pointer]: + - /url: /studio/releases + - link "공개 사이트 보기" [ref=f6e16] [cursor=pointer]: + - /url: / + - button "로그아웃" [ref=f6e17] + - main [ref=f6e18]: + - generic [ref=f6e51]: + - tablist "문서 편집 화면" [ref=f6e52]: + - tab "편집" [selected] [ref=f6e53] + - tab "즉시 미리보기" [ref=f6e54] + - generic [ref=f6e55]: + - tabpanel "편집" [ref=f6e57]: + - generic [ref=f6e58]: + - paragraph [ref=f6e59]: QUESTION · VERSION 1 + - heading "문서 편집" [level=1] [ref=f6e60] + - paragraph [ref=f6e61]: 제목 없는 작업본 + - region [ref=f6e62]: + - generic [ref=f6e63]: + - paragraph [ref=f6e64]: DOCUMENT + - heading "기본 정보" [level=2] [ref=f6e65] + - generic [ref=f6e66]: + - generic [ref=f6e67]: + - generic [ref=f6e68]: 제목 + - textbox "제목" [ref=f6e69] + - generic [ref=f6e70]: + - generic [ref=f6e71]: slug + - textbox "slug" [ref=f6e72]: + - /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈) + - generic [ref=f6e73]: + - generic [ref=f6e74]: 요약 + - textbox "요약" [ref=f6e75] + - generic [ref=f6e76]: + - generic [ref=f6e77]: Topic + - combobox "Topic" [ref=f6e78]: + - option "선택하지 않음" [selected] + - option "OAuth/OIDC 인증 경계" + - generic [ref=f6e79]: + - generic [ref=f6e80]: Project + - combobox "Project" [ref=f6e81]: + - option "미지정" [selected] + - option "Backend Clean Architecture" + - option "KeyCloak Patterns" + - option "Liner N + 1문제" + - group "관계" [ref=f6e82]: + - paragraph [ref=f6e84]: 연결한 공개 기록이 없습니다. + - button "관계 추가" [ref=f6e85] + - region [ref=f6e86]: + - generic [ref=f6e87]: + - paragraph [ref=f6e88]: QUESTION + - heading "판단과 다음 검증" [level=2] [ref=f6e89] + - generic [ref=f6e90]: + - generic [ref=f6e91]: 질문 상태 + - combobox "질문 상태" [ref=f6e92]: + - option "아직 정하지 않음" + - option "OPEN" [selected] + - option "RESOLVED" + - group "사실" [ref=f6e93]: + - paragraph [ref=f6e95]: 아직 입력한 항목이 없습니다. + - button "사실 추가" [ref=f6e96] + - group "가정" [ref=f6e97]: + - paragraph [ref=f6e99]: 아직 입력한 항목이 없습니다. + - button "가정 추가" [ref=f6e100] + - group "미지수" [ref=f6e101]: + - paragraph [ref=f6e103]: 아직 입력한 항목이 없습니다. + - button "미지수 추가" [ref=f6e104] + - group "제약" [ref=f6e105]: + - paragraph [ref=f6e107]: 아직 입력한 항목이 없습니다. + - button "제약 추가" [ref=f6e108] + - group "선택지" [ref=f6e109]: + - paragraph [ref=f6e111]: 아직 입력한 선택지가 없습니다. + - button "선택지 추가" [ref=f6e112] + - generic [ref=f6e113]: + - generic [ref=f6e114]: 다음 검증 + - textbox "다음 검증" [ref=f6e115] + - complementary [ref=f6e116]: + - paragraph [ref=f6e117]: WORKING COPY + - heading "작업 상태" [level=2] [ref=f6e118] + - status "편집 상태" [ref=f6e119]: 저장됨 + - generic [ref=f6e120]: + - generic [ref=f6e121]: + - term [ref=f6e122]: 저장 버전 + - definition [ref=f6e123]: "1" + - generic [ref=f6e124]: + - term [ref=f6e125]: 종류 + - definition [ref=f6e126]: QUESTION + - button "저장" [disabled] [ref=f6e127] + - navigation "게시까지의 단계" [ref=f6e128]: + - list [ref=f6e129]: + - listitem [ref=f6e130]: + - generic [ref=f6e131]: "1" + - link "검증" [ref=f6e132] [cursor=pointer]: + - /url: /studio/documents/f54170bb-e1a2-466d-9d1c-39563e03266c/validation + - listitem [ref=f6e133]: + - text: → + - generic [ref=f6e134]: "2" + - link "미리보기" [ref=f6e135] [cursor=pointer]: + - /url: /studio/documents/f54170bb-e1a2-466d-9d1c-39563e03266c/preview + - listitem [ref=f6e136]: + - text: → + - generic [ref=f6e137]: "3" + - link "게시" [ref=f6e138] [cursor=pointer]: + - /url: /studio/documents/f54170bb-e1a2-466d-9d1c-39563e03266c/publish + - paragraph [ref=f6e139]: 불완전한 초안도 저장할 수 있습니다. 게시 가능 여부는 이후 검증 단계에서 확인합니다. + - paragraph [ref=f6e50]: Question 작업본을 만들었습니다. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-21T10-09-30-337Z.yml b/.playwright-mcp/page-2026-08-21T10-09-30-337Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/.playwright-mcp/page-2026-08-21T10-09-36-210Z.yml b/.playwright-mcp/page-2026-08-21T10-09-36-210Z.yml new file mode 100644 index 0000000..7ed9a21 --- /dev/null +++ b/.playwright-mcp/page-2026-08-21T10-09-36-210Z.yml @@ -0,0 +1,99 @@ +- generic [ref=f7e3]: + - link "본문으로 건너뛰기" [ref=f7e4] [cursor=pointer]: + - /url: "#main-content" + - banner [ref=f7e5]: + - generic [ref=f7e6]: + - link "TechLog Studio" [ref=f7e7] [cursor=pointer]: + - /url: /studio + - text: TechLog + - generic [ref=f7e8]: Studio + - navigation "Studio 주 탐색" [ref=f7e10]: + - link "작업본" [ref=f7e11] [cursor=pointer]: + - /url: /studio/documents + - link "게시 기록" [ref=f7e12] [cursor=pointer]: + - /url: /studio/publications + - link "새 문서" [ref=f7e13] [cursor=pointer]: + - /url: /studio/documents/new + - link "주제·프로젝트" [ref=f7e14] [cursor=pointer]: + - /url: /studio/taxonomy + - link "릴리즈" [ref=f7e15] [cursor=pointer]: + - /url: /studio/releases + - link "공개 사이트 보기" [ref=f7e16] [cursor=pointer]: + - /url: / + - button "로그아웃" [ref=f7e17] + - main [ref=f7e18]: + - generic [ref=f7e19]: + - generic [ref=f7e20]: + - generic [ref=f7e21]: + - paragraph [ref=f7e22]: WORKING COPIES + - heading "작업본" [level=1] [ref=f7e23] + - paragraph [ref=f7e24]: 세션에 있는 Case, Reference, Question, Decision을 찾고 다음 작업으로 이동합니다. + - link "새 문서" [ref=f7e25] [cursor=pointer]: + - /url: /studio/documents/new + - region "작업본 검색과 필터" [ref=f7e26]: + - search [ref=f7e27]: + - generic [ref=f7e28]: 검색 + - generic [ref=f7e29]: + - searchbox "검색" [ref=f7e30] + - button "검색" [ref=f7e31] + - generic [ref=f7e32]: + - text: 종류 + - combobox "종류" [ref=f7e33]: + - option "전체" [selected] + - option "Case" + - option "Reference" + - option "Question" + - option "Decision" + - generic [ref=f7e34]: + - text: 상태 + - combobox "상태" [ref=f7e35]: + - option "전체" [selected] + - option "게시 전" + - option "게시 중" + - option "게시 취소" + - paragraph [ref=f7e36]: + - generic [ref=f7e37]: 2개 표시 중 + - generic [ref=f7e38]: + - article [ref=f7e39]: + - paragraph [ref=f7e40]: Case + - generic [ref=f7e41]: + - heading [level=2] [ref=f7e42]: + - link "게시 흐름 확인" [ref=f7e43] [cursor=pointer]: + - /url: /studio/documents/b09f168b-b205-478f-b60f-83872d1b4d01/edit + - paragraph [ref=f7e44]: 프로젝트 미지정 + - generic [ref=f7e45]: + - generic [ref=f7e46]: + - term [ref=f7e47]: 상태 + - definition [ref=f7e48]: 게시 취소 + - generic [ref=f7e49]: + - term [ref=f7e50]: 다음 + - definition [ref=f7e51]: + - link "검증하기" [ref=f7e52] [cursor=pointer]: + - /url: /studio/documents/b09f168b-b205-478f-b60f-83872d1b4d01/validation + - generic [ref=f7e53]: + - term [ref=f7e54]: 수정 + - definition [ref=f7e55]: + - time [ref=f7e56]: 2026. 8. 21. + - button "삭제" [ref=f7e57] + - article [ref=f7e58]: + - paragraph [ref=f7e59]: Case + - generic [ref=f7e60]: + - heading [level=2] [ref=f7e61]: + - link "브라우저에서 토큰을 어디까지 다뤄야 하는가..?" [ref=f7e62] [cursor=pointer]: + - /url: /studio/documents/f363edc8-2c13-4995-acda-934237034a85/edit + - paragraph [ref=f7e63]: 프로젝트 미지정 + - generic [ref=f7e64]: + - generic [ref=f7e65]: + - term [ref=f7e66]: 상태 + - definition [ref=f7e67]: 게시 전 + - generic [ref=f7e68]: + - term [ref=f7e69]: 다음 + - definition [ref=f7e70]: + - link "검증하기" [ref=f7e71] [cursor=pointer]: + - /url: /studio/documents/f363edc8-2c13-4995-acda-934237034a85/validation + - generic [ref=f7e72]: + - term [ref=f7e73]: 수정 + - definition [ref=f7e74]: + - time [ref=f7e75]: 2026. 8. 21. + - button "삭제" [ref=f7e76] + - paragraph [ref=f7e77]: 작업본 제목 없음 을(를) 삭제했습니다. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-21T10-09-52-861Z.yml b/.playwright-mcp/page-2026-08-21T10-09-52-861Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/.playwright-mcp/page-2026-08-21T10-10-12-379Z.yml b/.playwright-mcp/page-2026-08-21T10-10-12-379Z.yml new file mode 100644 index 0000000..fa71e68 --- /dev/null +++ b/.playwright-mcp/page-2026-08-21T10-10-12-379Z.yml @@ -0,0 +1,106 @@ +- generic [ref=f8e74]: + - link "본문으로 건너뛰기" [ref=f8e75] [cursor=pointer]: + - /url: "#main-content" + - banner [ref=f8e76]: + - generic [ref=f8e77]: + - link "TechLog Studio" [ref=f8e78] [cursor=pointer]: + - /url: /studio + - text: TechLog + - generic [ref=f8e79]: Studio + - navigation "Studio 주 탐색" [ref=f8e81]: + - link "작업본" [ref=f8e82] [cursor=pointer]: + - /url: /studio/documents + - link "게시 기록" [ref=f8e83] [cursor=pointer]: + - /url: /studio/publications + - link "새 문서" [ref=f8e84] [cursor=pointer]: + - /url: /studio/documents/new + - link "주제·프로젝트" [ref=f8e85] [cursor=pointer]: + - /url: /studio/taxonomy + - link "릴리즈" [ref=f8e86] [cursor=pointer]: + - /url: /studio/releases + - link "공개 사이트 보기" [ref=f8e87] [cursor=pointer]: + - /url: / + - button "로그아웃" [ref=f8e88] + - main [ref=f8e89]: + - generic [ref=f8e1]: + - generic [ref=f8e3]: + - paragraph [ref=f8e4]: TAXONOMY + - heading "주제와 프로젝트" [level=1] [ref=f8e5] + - paragraph [ref=f8e6]: 문서를 게시하려면 주제가 필요합니다. 여기서 만들고 정리합니다. + - region "주제 만들기" [ref=f8e7]: + - generic [ref=f8e8]: + - generic [ref=f8e9]: 새 주제 + - generic [ref=f8e10]: + - textbox "새 주제" [ref=f8e11]: + - /placeholder: 주제 이름 + - textbox "주제 slug" [ref=f8e12]: + - /placeholder: slug (비우면 이름에서 생성) + - button "추가" [ref=f8e13] + - generic [ref=f8e14]: + - generic [ref=f8e15]: 새 프로젝트 + - generic [ref=f8e16]: + - textbox "새 프로젝트" [ref=f8e17]: + - /placeholder: 프로젝트 이름 + - button "추가" [ref=f8e18] + - paragraph [ref=f8e90]: 2개의 주제 + - generic [ref=f8e91]: + - article [ref=f8e92]: + - paragraph [ref=f8e93]: TOPIC + - generic [ref=f8e94]: + - heading "삭제 시험 주제" [level=2] [ref=f8e95] + - paragraph [ref=f8e96]: sakje-siheom-juje + - generic [ref=f8e98]: + - term [ref=f8e99]: 상태 + - definition [ref=f8e100]: 사용 중 + - button "삭제" [ref=f8e101] + - article [ref=f8e102]: + - paragraph [ref=f8e103]: TOPIC + - generic [ref=f8e104]: + - heading "OAuth/OIDC 인증 경계" [level=2] [ref=f8e105] + - paragraph [ref=f8e106]: oauth-oidc-auth-boundary + - generic [ref=f8e108]: + - term [ref=f8e109]: 상태 + - definition [ref=f8e110]: 사용 중 + - button "삭제" [ref=f8e111] + - paragraph [ref=f8e112]: 3개의 프로젝트 + - generic [ref=f8e113]: + - article [ref=f8e114]: + - paragraph [ref=f8e115]: PROJECT + - generic [ref=f8e116]: + - heading "Liner N + 1문제" [level=2] [ref=f8e117] + - paragraph [ref=f8e118]: 목표 미지정 + - generic [ref=f8e119]: + - generic [ref=f8e120]: + - term [ref=f8e121]: 단계 + - definition [ref=f8e122]: RESEARCH + - generic [ref=f8e123]: + - term [ref=f8e124]: 공개 + - definition [ref=f8e125]: PRIVATE + - button "삭제" [ref=f8e126] + - article [ref=f8e127]: + - paragraph [ref=f8e128]: PROJECT + - generic [ref=f8e129]: + - heading "KeyCloak Patterns" [level=2] [ref=f8e130] + - paragraph [ref=f8e131]: 목표 미지정 + - generic [ref=f8e132]: + - generic [ref=f8e133]: + - term [ref=f8e134]: 단계 + - definition [ref=f8e135]: RESEARCH + - generic [ref=f8e136]: + - term [ref=f8e137]: 공개 + - definition [ref=f8e138]: PRIVATE + - button "삭제" [ref=f8e139] + - article [ref=f8e140]: + - paragraph [ref=f8e141]: PROJECT + - generic [ref=f8e142]: + - heading "Backend Clean Architecture" [level=2] [ref=f8e143] + - paragraph [ref=f8e144]: 목표 미지정 + - generic [ref=f8e145]: + - generic [ref=f8e146]: + - term [ref=f8e147]: 단계 + - definition [ref=f8e148]: RESEARCH + - generic [ref=f8e149]: + - term [ref=f8e150]: 공개 + - definition [ref=f8e151]: PRIVATE + - button "삭제" [ref=f8e152] + - paragraph [ref=f8e153]: 주제 삭제 시험 주제 을(를) 만들었습니다. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-21T10-10-26-016Z.yml b/.playwright-mcp/page-2026-08-21T10-10-26-016Z.yml new file mode 100644 index 0000000..1db5b84 --- /dev/null +++ b/.playwright-mcp/page-2026-08-21T10-10-26-016Z.yml @@ -0,0 +1,96 @@ +- generic [ref=f8e74]: + - link "본문으로 건너뛰기" [ref=f8e75] [cursor=pointer]: + - /url: "#main-content" + - banner [ref=f8e76]: + - generic [ref=f8e77]: + - link "TechLog Studio" [ref=f8e78] [cursor=pointer]: + - /url: /studio + - text: TechLog + - generic [ref=f8e79]: Studio + - navigation "Studio 주 탐색" [ref=f8e81]: + - link "작업본" [ref=f8e82] [cursor=pointer]: + - /url: /studio/documents + - link "게시 기록" [ref=f8e83] [cursor=pointer]: + - /url: /studio/publications + - link "새 문서" [ref=f8e84] [cursor=pointer]: + - /url: /studio/documents/new + - link "주제·프로젝트" [ref=f8e85] [cursor=pointer]: + - /url: /studio/taxonomy + - link "릴리즈" [ref=f8e86] [cursor=pointer]: + - /url: /studio/releases + - link "공개 사이트 보기" [ref=f8e87] [cursor=pointer]: + - /url: / + - button "로그아웃" [ref=f8e88] + - main [ref=f8e89]: + - generic [ref=f8e1]: + - generic [ref=f8e3]: + - paragraph [ref=f8e4]: TAXONOMY + - heading "주제와 프로젝트" [level=1] [ref=f8e5] + - paragraph [ref=f8e6]: 문서를 게시하려면 주제가 필요합니다. 여기서 만들고 정리합니다. + - region "주제 만들기" [ref=f8e7]: + - generic [ref=f8e8]: + - generic [ref=f8e9]: 새 주제 + - generic [ref=f8e10]: + - textbox "새 주제" [ref=f8e11]: + - /placeholder: 주제 이름 + - textbox "주제 slug" [ref=f8e12]: + - /placeholder: slug (비우면 이름에서 생성) + - button "추가" [ref=f8e13] + - generic [ref=f8e14]: + - generic [ref=f8e15]: 새 프로젝트 + - generic [ref=f8e16]: + - textbox "새 프로젝트" [ref=f8e17]: + - /placeholder: 프로젝트 이름 + - button "추가" [ref=f8e18] + - paragraph [ref=f8e154]: 1개의 주제 + - article [ref=f8e156]: + - paragraph [ref=f8e157]: TOPIC + - generic [ref=f8e158]: + - heading "OAuth/OIDC 인증 경계" [level=2] [ref=f8e159] + - paragraph [ref=f8e160]: oauth-oidc-auth-boundary + - generic [ref=f8e162]: + - term [ref=f8e163]: 상태 + - definition [ref=f8e164]: 사용 중 + - button "삭제" [ref=f8e165] + - paragraph [ref=f8e166]: 3개의 프로젝트 + - generic [ref=f8e167]: + - article [ref=f8e168]: + - paragraph [ref=f8e169]: PROJECT + - generic [ref=f8e170]: + - heading "Liner N + 1문제" [level=2] [ref=f8e171] + - paragraph [ref=f8e172]: 목표 미지정 + - generic [ref=f8e173]: + - generic [ref=f8e174]: + - term [ref=f8e175]: 단계 + - definition [ref=f8e176]: RESEARCH + - generic [ref=f8e177]: + - term [ref=f8e178]: 공개 + - definition [ref=f8e179]: PRIVATE + - button "삭제" [ref=f8e180] + - article [ref=f8e181]: + - paragraph [ref=f8e182]: PROJECT + - generic [ref=f8e183]: + - heading "KeyCloak Patterns" [level=2] [ref=f8e184] + - paragraph [ref=f8e185]: 목표 미지정 + - generic [ref=f8e186]: + - generic [ref=f8e187]: + - term [ref=f8e188]: 단계 + - definition [ref=f8e189]: RESEARCH + - generic [ref=f8e190]: + - term [ref=f8e191]: 공개 + - definition [ref=f8e192]: PRIVATE + - button "삭제" [ref=f8e193] + - article [ref=f8e194]: + - paragraph [ref=f8e195]: PROJECT + - generic [ref=f8e196]: + - heading "Backend Clean Architecture" [level=2] [ref=f8e197] + - paragraph [ref=f8e198]: 목표 미지정 + - generic [ref=f8e199]: + - generic [ref=f8e200]: + - term [ref=f8e201]: 단계 + - definition [ref=f8e202]: RESEARCH + - generic [ref=f8e203]: + - term [ref=f8e204]: 공개 + - definition [ref=f8e205]: PRIVATE + - button "삭제" [ref=f8e206] + - paragraph [ref=f8e153]: 주제 삭제 시험 주제 을(를) 삭제했습니다. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-21T10-10-58-198Z.yml b/.playwright-mcp/page-2026-08-21T10-10-58-198Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/.playwright-mcp/page-2026-08-21T10-11-03-556Z.yml b/.playwright-mcp/page-2026-08-21T10-11-03-556Z.yml new file mode 100644 index 0000000..4e7a9ad --- /dev/null +++ b/.playwright-mcp/page-2026-08-21T10-11-03-556Z.yml @@ -0,0 +1,100 @@ +- generic [ref=f9e3]: + - link "본문으로 건너뛰기" [ref=f9e4] [cursor=pointer]: + - /url: "#main-content" + - banner [ref=f9e5]: + - generic [ref=f9e6]: + - link "TechLog Studio" [ref=f9e7] [cursor=pointer]: + - /url: /studio + - text: TechLog + - generic [ref=f9e8]: Studio + - navigation "Studio 주 탐색" [ref=f9e10]: + - link "작업본" [ref=f9e11] [cursor=pointer]: + - /url: /studio/documents + - link "게시 기록" [ref=f9e12] [cursor=pointer]: + - /url: /studio/publications + - link "새 문서" [ref=f9e13] [cursor=pointer]: + - /url: /studio/documents/new + - link "주제·프로젝트" [ref=f9e14] [cursor=pointer]: + - /url: /studio/taxonomy + - link "릴리즈" [ref=f9e15] [cursor=pointer]: + - /url: /studio/releases + - link "공개 사이트 보기" [ref=f9e16] [cursor=pointer]: + - /url: / + - button "로그아웃" [ref=f9e17] + - main [ref=f9e18]: + - generic [ref=f9e19]: + - generic [ref=f9e20]: + - generic [ref=f9e21]: + - paragraph [ref=f9e22]: WORKING COPIES + - heading "작업본" [level=1] [ref=f9e23] + - paragraph [ref=f9e24]: 세션에 있는 Case, Reference, Question, Decision을 찾고 다음 작업으로 이동합니다. + - link "새 문서" [ref=f9e25] [cursor=pointer]: + - /url: /studio/documents/new + - region "작업본 검색과 필터" [ref=f9e26]: + - search [ref=f9e27]: + - generic [ref=f9e28]: 검색 + - generic [ref=f9e29]: + - searchbox "검색" [ref=f9e30] + - button "검색" [ref=f9e31] + - generic [ref=f9e32]: + - text: 종류 + - combobox "종류" [ref=f9e33]: + - option "전체" [selected] + - option "Case" + - option "Reference" + - option "Question" + - option "Decision" + - generic [ref=f9e34]: + - text: 상태 + - combobox "상태" [ref=f9e35]: + - option "전체" [selected] + - option "게시 전" + - option "게시 중" + - option "게시 취소" + - paragraph [ref=f9e36]: + - generic [ref=f9e37]: 2개 표시 중 + - alert [ref=f9e38]: 삭제하지 못했습니다. 게시 중이거나, 이 기록을 참조하는 곳이 있거나, 다른 곳에서 먼저 수정되었을 수 있습니다. + - generic [ref=f9e39]: + - article [ref=f9e40]: + - paragraph [ref=f9e41]: Case + - generic [ref=f9e42]: + - heading [level=2] [ref=f9e43]: + - link "게시 흐름 확인" [ref=f9e44] [cursor=pointer]: + - /url: /studio/documents/b09f168b-b205-478f-b60f-83872d1b4d01/edit + - paragraph [ref=f9e45]: 프로젝트 미지정 + - generic [ref=f9e46]: + - generic [ref=f9e47]: + - term [ref=f9e48]: 상태 + - definition [ref=f9e49]: 게시 취소 + - generic [ref=f9e50]: + - term [ref=f9e51]: 다음 + - definition [ref=f9e52]: + - link "검증하기" [ref=f9e53] [cursor=pointer]: + - /url: /studio/documents/b09f168b-b205-478f-b60f-83872d1b4d01/validation + - generic [ref=f9e54]: + - term [ref=f9e55]: 수정 + - definition [ref=f9e56]: + - time [ref=f9e57]: 2026. 8. 21. + - button "삭제" [ref=f9e58] + - article [ref=f9e59]: + - paragraph [ref=f9e60]: Case + - generic [ref=f9e61]: + - heading [level=2] [ref=f9e62]: + - link "브라우저에서 토큰을 어디까지 다뤄야 하는가..?" [ref=f9e63] [cursor=pointer]: + - /url: /studio/documents/f363edc8-2c13-4995-acda-934237034a85/edit + - paragraph [ref=f9e64]: 프로젝트 미지정 + - generic [ref=f9e65]: + - generic [ref=f9e66]: + - term [ref=f9e67]: 상태 + - definition [ref=f9e68]: 게시 전 + - generic [ref=f9e69]: + - term [ref=f9e70]: 다음 + - definition [ref=f9e71]: + - link "검증하기" [ref=f9e72] [cursor=pointer]: + - /url: /studio/documents/f363edc8-2c13-4995-acda-934237034a85/validation + - generic [ref=f9e73]: + - term [ref=f9e74]: 수정 + - definition [ref=f9e75]: + - time [ref=f9e76]: 2026. 8. 21. + - button "삭제" [ref=f9e77] + - paragraph [ref=f9e78] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-21T10-18-11-148Z.yml b/.playwright-mcp/page-2026-08-21T10-18-11-148Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/.playwright-mcp/page-2026-08-21T10-18-21-158Z.yml b/.playwright-mcp/page-2026-08-21T10-18-21-158Z.yml new file mode 100644 index 0000000..e932986 --- /dev/null +++ b/.playwright-mcp/page-2026-08-21T10-18-21-158Z.yml @@ -0,0 +1,50 @@ +- generic [ref=f10e19]: + - link "본문으로 건너뛰기" [ref=f10e20] [cursor=pointer]: + - /url: "#main-content" + - banner [ref=f10e21]: + - generic [ref=f10e22]: + - link "TechLog Studio" [ref=f10e23] [cursor=pointer]: + - /url: /studio + - text: TechLog + - generic [ref=f10e24]: Studio + - navigation "Studio 주 탐색" [ref=f10e26]: + - link "작업본" [ref=f10e27] [cursor=pointer]: + - /url: /studio/documents + - link "게시 기록" [ref=f10e28] [cursor=pointer]: + - /url: /studio/publications + - link "새 문서" [ref=f10e29] [cursor=pointer]: + - /url: /studio/documents/new + - link "주제·프로젝트" [ref=f10e30] [cursor=pointer]: + - /url: /studio/taxonomy + - link "릴리즈" [ref=f10e31] [cursor=pointer]: + - /url: /studio/releases + - link "공개 사이트 보기" [ref=f10e32] [cursor=pointer]: + - /url: / + - button "로그아웃" [ref=f10e33] + - main [ref=f10e34]: + - generic [ref=f10e1]: + - generic [ref=f10e2]: + - paragraph [ref=f10e3]: ASSET LIBRARY + - heading "Asset" [level=1] [ref=f10e4] + - paragraph [ref=f10e5]: 업로드한 Asset을 검색하고 사용처를 확인하며, 사용하지 않는 Asset을 정리합니다. + - region "Asset 검색 도구" [ref=f10e6]: + - search [ref=f10e7]: + - generic [ref=f10e8]: Asset 검색 + - generic [ref=f10e9]: + - searchbox "Asset 검색" [ref=f10e10] + - button "검색" [ref=f10e11] + - status + - status [ref=f10e12]: 1개의 Asset + - list [ref=f10e13]: + - listitem [ref=f10e14]: + - button "screenshot-from-2026-08-21-18-04-49-72f1f9c6" [ref=f10e15] + - text: READY + - generic [ref=f10e16]: 사용 0건 + - generic "screenshot-from-2026-08-21-18-04-49-72f1f9c6 상세" [ref=f10e35]: + - heading "screenshot-from-2026-08-21-18-04-49-72f1f9c6" [active] [level=2] [ref=f10e36] + - paragraph [ref=f10e37]: READY + - paragraph [ref=f10e38]: 사용 중인 문서가 없습니다. + - generic [ref=f10e39]: + - button "삭제" [ref=f10e40] + - button "닫기" [ref=f10e41] + - paragraph [ref=f10e42] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-21T10-18-37-556Z.yml b/.playwright-mcp/page-2026-08-21T10-18-37-556Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/.playwright-mcp/page-2026-08-21T10-19-02-154Z.yml b/.playwright-mcp/page-2026-08-21T10-19-02-154Z.yml new file mode 100644 index 0000000..e69de29 diff --git a/public/release-manifest.json b/public/release-manifest.json index 3ace26e..d6e1080 100644 --- a/public/release-manifest.json +++ b/public/release-manifest.json @@ -38,28 +38,28 @@ }, "contractSet": { "setAlgorithm": "CA_CONTRACT_SET_V1", - "setDigest": "sha256:38329cfe645e1d6cbfc7a9bb3e20e23b579a286ad0d1254982c70de5c6055ae8", + "setDigest": "sha256:8b5bcbc235bc825483002374f84d433e2668a613e270e6558934fa277bd62762", "packages": [ { "packageId": "@tech-log/management-contract", "version": "1.0.0", "digest": "sha256:d19ae7c4fbcac924a356bbb0cc1a46a4046ecec701158ca0a9b7cc089bbaf878", "runtimeProtocolVersion": 1, - "sourceRevision": "65a04fc" + "sourceRevision": "b195b29" }, { "packageId": "@tech-log/public-contract", "version": "2.0.0", "digest": "sha256:6575a09317a1ffe951747b12102ad2cf884110007426a45d6f59a53b65612d59", "runtimeProtocolVersion": 1, - "sourceRevision": "65a04fc" + "sourceRevision": "b195b29" }, { "packageId": "@tech-log/studio-contract", "version": "3.0.0", - "digest": "sha256:5229865c3d242f19d75030d3f524a44dfebbf444324068d6ae88e43b8047dba4", + "digest": "sha256:674327a82951fd4a1bc2594c858072dfcd0b9abe7c63198283da5d8c92a04326", "runtimeProtocolVersion": 1, - "sourceRevision": "65a04fc" + "sourceRevision": "b195b29" } ] } diff --git a/src/features/tech-log/adapters/http/http-management-gateway.ts b/src/features/tech-log/adapters/http/http-management-gateway.ts index 360e119..ab52c47 100644 --- a/src/features/tech-log/adapters/http/http-management-gateway.ts +++ b/src/features/tech-log/adapters/http/http-management-gateway.ts @@ -10,6 +10,7 @@ import type { TopicEdit, } from "../../contracts/management/contract.ts"; import type { ManagementGateway } from "../../application/ports/management-gateway.ts"; +import { ManagementGatewayError } from "../../application/ports/management-gateway-error.ts"; import type { StudioOperationExecutor } from "./http-studio-gateway.ts"; export type { ManagementGateway }; @@ -23,17 +24,10 @@ const ROUTE_ID = "TECH_LOG_STUDIO"; * Studio 쪽 상태 처리를 그대로 쓴다. 여기서 Result 로 감싸면 이 표면만 다른 규약이 된다. */ -export class ManagementGatewayError extends Error { - readonly operationId: string; - readonly code: string; - - constructor(operationId: string, code: string) { - super(`${operationId}: ${code}`); - this.name = "ManagementGatewayError"; - this.operationId = operationId; - this.code = code; - } -} +export { + ManagementGatewayError, + managementFailureMessage, +} from "../../application/ports/management-gateway-error.ts"; export function createHttpManagementGateway( deps: Readonly<{ operations: StudioOperationExecutor }>, @@ -46,7 +40,11 @@ export function createHttpManagementGateway( // the code under `error` — reading `problem.code` found nothing and every // failure surfaced as the literal "PROBLEM", matching no i18n key. const body = outcome.problem as - | Readonly<{ code?: unknown; error?: Readonly<{ code?: unknown }> }> + | Readonly<{ + code?: unknown; + detail?: unknown; + error?: Readonly<{ code?: unknown; message?: unknown }>; + }> | null; const code = typeof body?.code === "string" @@ -54,9 +52,15 @@ export function createHttpManagementGateway( : typeof body?.error?.code === "string" ? body.error.code : "PROBLEM"; - throw new ManagementGatewayError(operationId, code); + const detail = + typeof body?.error?.message === "string" + ? body.error.message + : typeof body?.detail === "string" + ? body.detail + : ""; + throw new ManagementGatewayError(operationId, code, detail); } - throw new ManagementGatewayError(operationId, outcome.kind); + throw new ManagementGatewayError(operationId, outcome.kind, ""); } return Object.freeze({ diff --git a/src/features/tech-log/adapters/mock/validate-working-copy.ts b/src/features/tech-log/adapters/mock/validate-working-copy.ts index 7172ade..cad2e77 100644 --- a/src/features/tech-log/adapters/mock/validate-working-copy.ts +++ b/src/features/tech-log/adapters/mock/validate-working-copy.ts @@ -170,16 +170,16 @@ export function validateWorkingCopy(document: WorkingCopy, dependencies: Validat const has = (id: string | null, type: CatalogEntry["type"]) => Boolean(id && dependencies.catalog.some((entry) => entry.id === id && entry.type === type)); if (blank(document.title)) error("TITLE_REQUIRED", "/title", "제목을 입력하세요."); if (blank(document.slug)) error("SLUG_REQUIRED", "/slug", "slug를 입력하세요."); else if (dependencies.documents.some((item) => item.id !== document.id && item.slug === document.slug)) error("SLUG_DUPLICATE", "/slug", "중복 slug입니다."); - if (blank(document.summary)) error("SUMMARY_REQUIRED", "/summary", "요약을 입력하세요."); - if (!has(document.topicId, "TOPIC")) error("TOPIC_REQUIRED", "/topicId", "Topic을 선택하세요."); + if (blank(document.summary)) warning("SUMMARY_REQUIRED", "/summary", "요약을 입력하세요."); + if (!has(document.topicId, "TOPIC")) warning("TOPIC_REQUIRED", "/topicId", "Topic을 선택하세요."); if (!document.projectId) { - if (document.kind === "PROJECT_DECISION") error("DECISION_PROJECT_REQUIRED", "/projectId", "Decision에는 Project가 필요합니다."); + if (document.kind === "PROJECT_DECISION") warning("DECISION_PROJECT_REQUIRED", "/projectId", "Decision에는 Project가 필요합니다."); else warning("PROJECT_MISSING", "/projectId", "Project 연결을 권장합니다."); } else if (!has(document.projectId, "PROJECT")) error("PROJECT_NOT_FOUND", "/projectId", "Project를 찾을 수 없습니다."); document.relations.forEach((relation, index) => { if (!has(relation.targetId, "RELATION")) error("RELATION_TARGET_NOT_FOUND", `/relations/${index}/targetId`, "관계 대상을 찾을 수 없습니다."); }); if (document.kind === "CASE") { - if (blank(document.problem)) error("CASE_PROBLEM_REQUIRED", "/problem", "문제를 입력하세요."); if (blank(document.conclusion)) error("CASE_CONCLUSION_REQUIRED", "/conclusion", "결론을 입력하세요."); - if (blank(document.bodyMarkdown)) error("CASE_BODY_REQUIRED", "/bodyMarkdown", "본문을 입력하세요."); + if (blank(document.problem)) warning("CASE_PROBLEM_REQUIRED", "/problem", "문제를 입력하세요."); if (blank(document.conclusion)) warning("CASE_CONCLUSION_REQUIRED", "/conclusion", "결론을 입력하세요."); + if (blank(document.bodyMarkdown)) warning("CASE_BODY_REQUIRED", "/bodyMarkdown", "본문을 입력하세요."); else try { const evidenceCatalog = [...dependencies.catalog, ...evidenceCatalogEntriesFromAssets(dependencies.assets)]; // The key gate is the preview projection's gate, verbatim: a resolvable @@ -202,20 +202,20 @@ export function validateWorkingCopy(document: WorkingCopy, dependencies: Validat } } } catch { error("CONTENT_FORMAT_INVALID", "/bodyMarkdown", "지원하는 문법을 사용하세요."); } - if (!document.lastVerifiedOn) error("LAST_VERIFIED_ON_REQUIRED", "/lastVerifiedOn", "검증일을 입력하세요."); else if (dependencies.now.getTime() - Date.parse(`${document.lastVerifiedOn}T00:00:00Z`) > 30 * 86_400_000) warning("VERIFICATION_OLDER_THAN_30_DAYS", "/lastVerifiedOn", "30일이 지났습니다."); + if (!document.lastVerifiedOn) warning("LAST_VERIFIED_ON_REQUIRED", "/lastVerifiedOn", "검증일을 입력하세요."); else if (dependencies.now.getTime() - Date.parse(`${document.lastVerifiedOn}T00:00:00Z`) > 30 * 86_400_000) warning("VERIFICATION_OLDER_THAN_30_DAYS", "/lastVerifiedOn", "30일이 지났습니다."); } else if (document.kind === "REFERENCE") { - if (blank(document.purpose)) error("REFERENCE_PURPOSE_REQUIRED", "/purpose", "목적을 입력하세요."); if (!document.rules.length) error("REFERENCE_RULE_REQUIRED", "/rules", "규칙이 필요합니다."); if (!document.applyWhen.length) error("REFERENCE_APPLY_WHEN_REQUIRED", "/applyWhen", "적용 조건이 필요합니다."); if (!document.verifiedOn) error("VERIFIED_ON_REQUIRED", "/verifiedOn", "검증일이 필요합니다."); if (!document.examples.length) warning("REFERENCE_EXAMPLE_MISSING", "/examples", "예시를 권장합니다."); + if (blank(document.purpose)) warning("REFERENCE_PURPOSE_REQUIRED", "/purpose", "목적을 입력하세요."); if (!document.rules.length) warning("REFERENCE_RULE_REQUIRED", "/rules", "규칙이 필요합니다."); if (!document.applyWhen.length) warning("REFERENCE_APPLY_WHEN_REQUIRED", "/applyWhen", "적용 조건이 필요합니다."); if (!document.verifiedOn) warning("VERIFIED_ON_REQUIRED", "/verifiedOn", "검증일이 필요합니다."); if (!document.examples.length) warning("REFERENCE_EXAMPLE_MISSING", "/examples", "예시를 권장합니다."); } else if (document.kind === "QUESTION") { - if (!document.questionStatus) error("QUESTION_STATUS_REQUIRED", "/questionStatus", "상태가 필요합니다."); if (blank(document.nextValidation)) error("NEXT_VALIDATION_REQUIRED", "/nextValidation", "다음 검증이 필요합니다."); if (!document.facts.length) error("QUESTION_FACT_REQUIRED", "/facts", "사실이 필요합니다."); + if (!document.questionStatus) warning("QUESTION_STATUS_REQUIRED", "/questionStatus", "상태가 필요합니다."); if (blank(document.nextValidation)) warning("NEXT_VALIDATION_REQUIRED", "/nextValidation", "다음 검증이 필요합니다."); if (!document.facts.length) warning("QUESTION_FACT_REQUIRED", "/facts", "사실이 필요합니다."); if (document.questionStatus === "OPEN") { if (!document.unknowns.length) error("QUESTION_UNKNOWN_REQUIRED", "/unknowns", "미확인 사항이 필요합니다."); if (document.resolution) error("OPEN_QUESTION_RESOLUTION_FORBIDDEN", "/resolution", "열린 질문에는 결론을 둘 수 없습니다."); } - if (document.questionStatus === "RESOLVED") { if (!document.resolution) error("QUESTION_RESOLUTION_REQUIRED", "/resolution", "해결 내용이 필요합니다."); else { if (blank(document.resolution.summary)) error("RESOLUTION_SUMMARY_REQUIRED", "/resolution/summary", "요약이 필요합니다."); if (!has(document.resolution.evidenceTargetId, "EVIDENCE")) error("RESOLUTION_EVIDENCE_REQUIRED", "/resolution/evidenceTargetId", "근거가 필요합니다."); if (blank(document.resolution.linkLabel)) error("RESOLUTION_LINK_LABEL_REQUIRED", "/resolution/linkLabel", "링크 문구가 필요합니다."); } } + if (document.questionStatus === "RESOLVED") { if (!document.resolution) warning("QUESTION_RESOLUTION_REQUIRED", "/resolution", "해결 내용이 필요합니다."); else { if (blank(document.resolution.summary)) warning("RESOLUTION_SUMMARY_REQUIRED", "/resolution/summary", "요약이 필요합니다."); if (!has(document.resolution.evidenceTargetId, "EVIDENCE")) error("RESOLUTION_EVIDENCE_REQUIRED", "/resolution/evidenceTargetId", "근거가 필요합니다."); if (blank(document.resolution.linkLabel)) warning("RESOLUTION_LINK_LABEL_REQUIRED", "/resolution/linkLabel", "링크 문구가 필요합니다."); } } if (document.options.length < 2) warning("QUESTION_OPTIONS_FEWER_THAN_TWO", "/options", "선택지 두 개를 권장합니다."); } else { - if (!document.decisionStatus) error("DECISION_STATUS_REQUIRED", "/decisionStatus", "결정 상태가 필요합니다."); - if (!document.decidedOn) error("DECIDED_ON_REQUIRED", "/decidedOn", "결정일이 필요합니다."); - if (blank(document.statement)) error("DECISION_STATEMENT_REQUIRED", "/statement", "결정문이 필요합니다."); - if (blank(document.rationale)) error("DECISION_RATIONALE_REQUIRED", "/rationale", "판단 이유가 필요합니다."); - if (!document.consequences.length) error("DECISION_CONSEQUENCE_REQUIRED", "/consequences", "영향이 하나 이상 필요합니다."); + if (!document.decisionStatus) warning("DECISION_STATUS_REQUIRED", "/decisionStatus", "결정 상태가 필요합니다."); + if (!document.decidedOn) warning("DECIDED_ON_REQUIRED", "/decidedOn", "결정일이 필요합니다."); + if (blank(document.statement)) warning("DECISION_STATEMENT_REQUIRED", "/statement", "결정문이 필요합니다."); + if (blank(document.rationale)) warning("DECISION_RATIONALE_REQUIRED", "/rationale", "판단 이유가 필요합니다."); + if (!document.consequences.length) warning("DECISION_CONSEQUENCE_REQUIRED", "/consequences", "영향이 하나 이상 필요합니다."); if (!document.relations.length) error("DECISION_EVIDENCE_REQUIRED", "/relations", "근거 기록이 하나 이상 필요합니다."); } const validatedAt = dependencies.now.toISOString(); diff --git a/src/features/tech-log/application/ports/management-gateway-error.ts b/src/features/tech-log/application/ports/management-gateway-error.ts new file mode 100644 index 0000000..f163a15 --- /dev/null +++ b/src/features/tech-log/application/ports/management-gateway-error.ts @@ -0,0 +1,32 @@ +/** + * 관리 표면의 실패. + * + *
어댑터가 아니라 포트 계층에 두는 이유는 화면이 이것을 읽어야 하기 때문이다 — `presentation` + * 은 `adapters` 를 보지 않는다 (`feature-presentation-does-not-know-outbound-adapters`). + * Studio 쪽 `studio-gateway-error.ts` 가 같은 이유로 같은 자리에 있다. + */ +export class ManagementGatewayError extends Error { + readonly operationId: string; + readonly code: string; + /** + * 서버가 준, 사람이 읽을 수 있는 이유. + * + *
이것이 없어서 화면은 실패할 때마다 자기가 지어낸 문구를 보여 줬다 — "게시 중이거나, + * 참조하는 곳이 있거나, 다른 곳에서 먼저 수정되었을 수 있습니다" 같은 추측 셋. 서버는 정확히 + * 무엇인지 알고 그것을 보내 주는데도 그랬고, 그래서 버전 충돌도 "사용 중" 으로 읽혔다. + */ + readonly detail: string; + + constructor(operationId: string, code: string, detail: string) { + super(`${operationId}: ${code}`); + this.name = "ManagementGatewayError"; + this.operationId = operationId; + this.code = code; + this.detail = detail; + } +} + +/** 실패에서 서버가 준 문구를 꺼낸다. 없으면 부른 쪽이 준 기본값을 쓴다. */ +export function managementFailureMessage(error: unknown, fallback: string): string { + return error instanceof ManagementGatewayError && error.detail ? error.detail : fallback; +} diff --git a/src/features/tech-log/contracts/management/canonical-source.json b/src/features/tech-log/contracts/management/canonical-source.json index 564c417..3dbbdab 100644 --- a/src/features/tech-log/contracts/management/canonical-source.json +++ b/src/features/tech-log/contracts/management/canonical-source.json @@ -2,7 +2,7 @@ "packageId": "@tech-log/management-contract", "version": "1.0.0", "digest": "sha256:d19ae7c4fbcac924a356bbb0cc1a46a4046ecec701158ca0a9b7cc089bbaf878", - "sourceRevision": "65a04fc", + "sourceRevision": "b195b29", "operationIds": [ "createCaseDraft", "getCaseForEdit", diff --git a/src/features/tech-log/contracts/public/canonical-source.json b/src/features/tech-log/contracts/public/canonical-source.json index 02a7f07..756e5b8 100644 --- a/src/features/tech-log/contracts/public/canonical-source.json +++ b/src/features/tech-log/contracts/public/canonical-source.json @@ -2,7 +2,7 @@ "packageId": "@tech-log/public-contract", "version": "2.0.0", "digest": "sha256:6575a09317a1ffe951747b12102ad2cf884110007426a45d6f59a53b65612d59", - "sourceRevision": "65a04fc", + "sourceRevision": "b195b29", "operationIds": [ "getPublicSite", "getPublicHome", diff --git a/src/features/tech-log/contracts/studio/canonical-source.json b/src/features/tech-log/contracts/studio/canonical-source.json index 5d04ea1..ec09ffa 100644 --- a/src/features/tech-log/contracts/studio/canonical-source.json +++ b/src/features/tech-log/contracts/studio/canonical-source.json @@ -1,8 +1,8 @@ { "packageId": "@tech-log/studio-contract", "version": "3.0.0", - "digest": "sha256:5229865c3d242f19d75030d3f524a44dfebbf444324068d6ae88e43b8047dba4", - "sourceRevision": "65a04fc", + "digest": "sha256:674327a82951fd4a1bc2594c858072dfcd0b9abe7c63198283da5d8c92a04326", + "sourceRevision": "b195b29", "operationIds": [ "getStudioSession", "getStudioDashboard", diff --git a/src/features/tech-log/contracts/studio/studio-api.openapi.yaml b/src/features/tech-log/contracts/studio/studio-api.openapi.yaml index f630344..8a2edbd 100644 --- a/src/features/tech-log/contracts/studio/studio-api.openapi.yaml +++ b/src/features/tech-log/contracts/studio/studio-api.openapi.yaml @@ -871,7 +871,7 @@ components: required: [id, text, order] properties: id: { type: string, format: uuid } - text: { type: string, minLength: 1, maxLength: 100000 } + text: { type: string, maxLength: 100000 } order: { type: integer, minimum: 0 } ReferenceRule: type: object @@ -1242,7 +1242,7 @@ components: kind: { $ref: "#/components/schemas/RecordKind" } slug: { type: string, minLength: 3, maxLength: 100, pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" } title: { type: string, minLength: 1, maxLength: 120 } - summary: { type: string, minLength: 1, maxLength: 300 } + summary: { type: string, maxLength: 300 } publicPath: { type: string, minLength: 1, maxLength: 500 } topic: { $ref: "#/components/schemas/DisplayTarget" } project: @@ -1257,7 +1257,7 @@ components: required: [type, text] properties: type: { type: string, enum: [TEXT] } - text: { type: string, minLength: 1, maxLength: 100000 } + text: { type: string, maxLength: 100000 } InlineContainer: type: object required: [type, children] @@ -1483,8 +1483,8 @@ components: required: [kind, problem, conclusion, environment, reproduction, lastVerifiedOn, bodyBlocks] properties: kind: { type: string, enum: [CASE] } - problem: { type: string, minLength: 1, maxLength: 100000 } - conclusion: { type: string, minLength: 1, maxLength: 100000 } + problem: { type: string, maxLength: 100000 } + conclusion: { type: string, maxLength: 100000 } environment: { type: string, maxLength: 100000 } reproduction: { type: string, maxLength: 100000 } lastVerifiedOn: { type: string, format: date } @@ -1497,7 +1497,7 @@ components: required: [kind, purpose, rules, applyWhen, exceptions, examples, verifiedOn] properties: kind: { type: string, enum: [REFERENCE] } - purpose: { type: string, minLength: 1, maxLength: 100000 } + purpose: { type: string, maxLength: 100000 } rules: { type: array, minItems: 1, maxItems: 50, items: { $ref: "#/components/schemas/ReferenceRule" } } applyWhen: { type: array, minItems: 1, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } exceptions: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } @@ -1508,7 +1508,7 @@ components: additionalProperties: false required: [summary, evidenceTarget, linkLabel] properties: - summary: { type: string, minLength: 1, maxLength: 100000 } + summary: { type: string, maxLength: 100000 } evidenceTarget: { $ref: "#/components/schemas/DisplayTarget" } linkLabel: { type: string, minLength: 1, maxLength: 120 } QuestionPublicRenderModel: @@ -1528,7 +1528,7 @@ components: unknowns: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } constraints: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } options: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/QuestionOption" } } - nextValidation: { type: string, minLength: 1, maxLength: 100000 } + nextValidation: { type: string, maxLength: 100000 } resolution: oneOf: - { $ref: "#/components/schemas/ResolvedQuestionResolution" } @@ -1543,8 +1543,8 @@ components: kind: { type: string, enum: [PROJECT_DECISION] } status: { type: string, enum: [PROPOSED, ADOPTED] } decidedOn: { type: string, format: date } - statement: { type: string, minLength: 1, maxLength: 100000 } - rationale: { type: string, minLength: 1, maxLength: 100000 } + statement: { type: string, maxLength: 100000 } + rationale: { type: string, maxLength: 100000 } consequences: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } PublicRenderModel: oneOf: diff --git a/src/features/tech-log/presentation/studio/components/document-list.tsx b/src/features/tech-log/presentation/studio/components/document-list.tsx index 2194128..bf16a2b 100644 --- a/src/features/tech-log/presentation/studio/components/document-list.tsx +++ b/src/features/tech-log/presentation/studio/components/document-list.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, type FormEvent } from "react"; import type { ListDocumentsQuery } from "../../../application/ports/studio-gateway.ts"; import type { DocumentPage } from "../../../contracts/studio/contract.ts"; +import { managementFailureMessage } from "../../../application/ports/management-gateway-error.ts"; import { useStudio } from "../use-studio.ts"; import { GuardedStudioLink } from "./guarded-studio-link.tsx"; @@ -98,10 +99,10 @@ export function DocumentList() { ? { ...current, items: current.items.filter((row) => row.id !== item.id) } : current, ); - } catch { - setDeleteError( - "삭제하지 못했습니다. 게시 중이거나, 이 기록을 참조하는 곳이 있거나, 다른 곳에서 먼저 수정되었을 수 있습니다.", - ); + } catch (error) { + // 서버는 무엇이 막았는지 정확히 알고 그것을 보내 준다. 예전에는 그 문구를 버리고 추측 셋을 + // 늘어놓아, 버전 충돌도 "사용 중" 으로 읽혔다. + setDeleteError(managementFailureMessage(error, "삭제하지 못했습니다.")); } finally { setDeletingId(null); } diff --git a/src/features/tech-log/presentation/studio/components/release-manager.tsx b/src/features/tech-log/presentation/studio/components/release-manager.tsx index 72c895f..cd3e5ae 100644 --- a/src/features/tech-log/presentation/studio/components/release-manager.tsx +++ b/src/features/tech-log/presentation/studio/components/release-manager.tsx @@ -5,6 +5,7 @@ import type { ReleaseIndexItem, ReleaseUpdateRequest, } from "../../../contracts/management/contract.ts"; +import { managementFailureMessage } from "../../../application/ports/management-gateway-error.ts"; import { useStudio } from "../use-studio.ts"; /** @@ -150,8 +151,8 @@ export function ReleaseManager() { setRequestAnnouncement(`릴리즈 ${title} 초안을 만들었습니다.`); setSelectedId(created.id); reload(); - } catch { - setError("릴리즈를 만들지 못했습니다."); + } catch (error) { + setError(managementFailureMessage(error, "릴리즈를 만들지 못했습니다.")); } finally { setPending(false); } @@ -180,8 +181,8 @@ export function ReleaseManager() { setDraft(toDraft(saved)); setRequestAnnouncement(`릴리즈 ${saved.versionLabel} 을(를) 저장했습니다.`); reload(); - } catch { - setError("저장하지 못했습니다. 같은 버전이 이미 있거나 다른 곳에서 먼저 수정되었을 수 있습니다."); + } catch (error) { + setError(managementFailureMessage(error, "저장하지 못했습니다.")); } finally { setPending(false); } @@ -214,8 +215,8 @@ export function ReleaseManager() { await managementGateway.archiveRelease(selectedId, draft.expectedVersion); setRequestAnnouncement("릴리즈를 공개에서 내렸습니다."); reload(); - } catch { - setError("공개에서 내리지 못했습니다."); + } catch (error) { + setError(managementFailureMessage(error, "공개에서 내리지 못했습니다.")); } finally { setPending(false); } @@ -230,8 +231,8 @@ export function ReleaseManager() { if (selectedId === release.id) setSelectedId(null); setRequestAnnouncement(`릴리즈 ${release.versionLabel} 을(를) 삭제했습니다.`); reload(); - } catch { - setError("삭제하지 못했습니다. 공개된 릴리즈는 삭제 대신 공개에서 내려야 합니다."); + } catch (error) { + setError(managementFailureMessage(error, "삭제하지 못했습니다.")); } finally { setPending(false); } diff --git a/src/features/tech-log/presentation/studio/components/taxonomy-manager.tsx b/src/features/tech-log/presentation/studio/components/taxonomy-manager.tsx index 879f5d2..93bba70 100644 --- a/src/features/tech-log/presentation/studio/components/taxonomy-manager.tsx +++ b/src/features/tech-log/presentation/studio/components/taxonomy-manager.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState, type FormEvent } from "react"; import type { ProjectIndexItem, TopicEdit } from "../../../contracts/management/contract.ts"; +import { managementFailureMessage } from "../../../application/ports/management-gateway-error.ts"; import { slugFromName } from "./slug-from-name.ts"; import { useStudio } from "../use-studio.ts"; @@ -75,8 +76,8 @@ export function TaxonomyManager() { setTopicSlug(""); setRequestAnnouncement(`주제 ${name} 을(를) 만들었습니다.`); reload(); - } catch { - setError("주제를 만들지 못했습니다. 같은 이름이나 slug 가 이미 있을 수 있습니다."); + } catch (error) { + setError(managementFailureMessage(error, "주제를 만들지 못했습니다.")); } finally { setPending(false); } @@ -97,8 +98,8 @@ export function TaxonomyManager() { setProjectTitle(""); setRequestAnnouncement(`프로젝트 ${title} 을(를) 만들었습니다.`); reload(); - } catch { - setError("프로젝트를 만들지 못했습니다."); + } catch (error) { + setError(managementFailureMessage(error, "프로젝트를 만들지 못했습니다.")); } finally { setPending(false); } @@ -112,8 +113,8 @@ export function TaxonomyManager() { await managementGateway.deleteTopic(topic.id, topic.version); setRequestAnnouncement(`주제 ${topic.name} 을(를) 삭제했습니다.`); reload(); - } catch { - setError("주제를 삭제하지 못했습니다. 이 주제를 쓰는 기록이 있을 수 있습니다."); + } catch (error) { + setError(managementFailureMessage(error, "주제를 삭제하지 못했습니다.")); } finally { setPending(false); } @@ -127,8 +128,8 @@ export function TaxonomyManager() { await managementGateway.deleteProject(project.id, project.version); setRequestAnnouncement(`프로젝트 ${project.name} 을(를) 삭제했습니다.`); reload(); - } catch { - setError("프로젝트를 삭제하지 못했습니다. 연결된 기록이 있을 수 있습니다."); + } catch (error) { + setError(managementFailureMessage(error, "프로젝트를 삭제하지 못했습니다.")); } finally { setPending(false); } @@ -210,7 +211,12 @@ export function TaxonomyManager() {