[{"data":1,"prerenderedAt":4341},["ShallowReactive",2],{"doc:\u002Fhistory\u002Fhistory.20260604":3},{"id":4,"title":5,"body":6,"description":337,"extension":4334,"meta":4335,"navigation":4336,"path":4337,"seo":4338,"stem":4339,"__hash__":4340},"docs\u002Fhistory\u002Fhistory.20260604.md","2026-06-04 — Hyperdrive Cloudflare Tunnel 전환 + 관리자단 핸드오프 17 페이지 풀세트 + NICE IPv6 진단 + WBS R2 편집 + 이메일 변경 버그 fix + 광고 수신 일시 기록",{"type":7,"value":8,"toc":4269},"minimark",[9,13,18,244,247,251,255,308,312,327,331,544,553,557,601,612,616,643,647,657,659,663,666,694,698,763,770,774,795,799,872,876,902,906,930,932,936,939,957,961,967,1310,1314,1376,1380,1568,1582,1586,1593,1622,1625,1629,1650,1657,1661,1669,1672,1676,1688,1692,1739,1741,1745,1748,1801,1805,1812,1876,1879,1882,1934,1937,1963,1970,1973,2032,2065,2069,2076,2182,2204,2225,2250,2253,2325,2328,2351,2355,2374,2378,2453,2455,2459,2463,2484,2488,2544,2548,2551,2612,2614,2640,2644,2651,2708,2712,2740,2744,2775,2779,2799,2803,2806,2901,2905,2925,2927,2929,2933,2936,2991,2995,3079,3083,3110,3117,3147,3151,3213,3217,3261,3265,3280,3282,3289,3292,3300,3304,3418,3422,3444,3448,3456,3458,3460,3464,3467,3498,3502,3522,3524,3594,3598,3608,3656,3664,3668,3698,3710,3871,3882,3886,3899,3903,3952,3956,3973,3975,3979,3982,3988,3992,4009,4020,4024,4068,4072,4123,4127,4145,4149,4179,4183,4206,4208,4212,4265],[10,11,5],"h1",{"id":12},"_2026-06-04-hyperdrive-cloudflare-tunnel-전환-관리자단-핸드오프-17-페이지-풀세트-nice-ipv6-진단-wbs-r2-편집-이메일-변경-버그-fix-광고-수신-일시-기록",[14,15,17],"h2",{"id":16},"한-줄-요약","한 줄 요약",[19,20,21,22,26,27,31,32,35,36,39,40,43,44,47,48,51,52,55,56,59,60,63,64,67,68,71,72,75,76,79,80,83,84,87,88,91,92,95,96,99,100,103,104,63,107,110,111,114,115,118,119,122,123,126,127,130,131,134,135,138,139,142,143,146,147,150,151,154,155,158,159,162,163,166,167,170,171,174,175,178,179,182,183,186,187,190,191,166,194,197,198,201,202,205,206,209,210,213,214,216,217,220,221,224,225,228,229,232,233,236,237,224,240,243],"p",{},"오늘 9건 처리. ",[23,24,25],"strong",{},"(§6)"," NHN Notification Hub 어댑터 신규(OAuth2 client_credentials → Bearer) + SMS·Email 라우트 활성화. Email real 발송 검증 통과(messageId 발급). + 서비스 담당자 이메일 변경 라우트(",[28,29,30],"code",{},"POST \u002Fme\u002Femail-change",", OTP + 비밀번호 검증, ",[23,33,34],{},"loginid 유지·email만 UPDATE",") 및 사용자단 다이얼로그 실 API 연동. ",[23,37,38],{},"(§7)"," WBS 현행화 — R2 정본(",[28,41,42],{},"wbs\u002Fwbs.json",") + ",[28,45,46],{},"doc\u002FWBS.md"," 동시 갱신. Step 5 진척 48% → 55%, 가중평균 약 47.5%. 5-2 신규 19\u002F20\u002F21(WBS R2 \u002F 이메일 변경 \u002F NHN OAuth 어댑터), 5-3-15(",[28,49,50],{},"\u002Fwbs"," 인라인 편집), 5-3C-7(완료 승급), 5-3C-20(이메일 변경 다이얼로그), 5-4-14\u002F15\u002F16(핸드오프 17 페이지 + dev 라벨 + 관리자 로고), 5-5-10\u002F11\u002F12(Hyperdrive Tunnel \u002F NHN Email real \u002F NHN SMS pending). ",[23,53,54],{},"(§8)"," 서비스 담당자 이메일 변경 — 두 가지 버그 fix. (a) ",[28,57,58],{},"\u002Fauth\u002Femail-code\u002Fverify"," 가 ",[28,61,62],{},"purpose='change_email'"," 에서 ",[28,65,66],{},"consumed_at"," 을 마킹해 직후 ",[28,69,70],{},"\u002Fme\u002Femail-change"," 의 OTP 재검증이 항상 실패하던 이중 소비 버그 → verify-only 분기로 수정. (b) 비밀번호·OTP 오류가 401 로 떨어져 ",[28,73,74],{},"useApi.onResponseError"," 가 토큰 만료로 오인하고 자동 로그아웃을 트리거하던 문제 → 새 헬퍼 ",[28,77,78],{},"errors.unprocessable()","(422) 도입해 본인 재인증 실패를 별도 코드로 분리. (c) 다이얼로그 ",[28,81,82],{},"submit()"," 이 ",[28,85,86],{},"submitting=true"," 만 켜고 부모의 ",[28,89,90],{},"close"," emit 을 기다리던 데드락 → ",[28,93,94],{},"onConfirm: (p) => Promise\u003Cvoid>"," 콜백 prop 패턴으로 전환해 다이얼로그가 자기 상태를 직접 관리. ",[23,97,98],{},"(§9)"," 광고성 메일 수신 동의\u002F거부 일시 기록 — DDL 0006 (",[28,101,102],{},"TB_COMPANY.ad_receive_at DATETIME NULL",") Aurora 적용 + ",[28,105,106],{},"PATCH \u002Fme\u002Fcompany",[28,108,109],{},"adReceive"," 변경 시 ",[28,112,113],{},"adReceiveAt: new Date()"," 동시 기록. 사용자단 패널에 ",[28,116,117],{},"[🕒] YYYY.MM.DD HH:mm 광고성 메일 수신에 동의\u002F거부함"," 회색 칩 노출 + 토스트 description 에 처리 일시 표기. 정보통신망법 50조 동의 증빙용. ",[23,120,121],{},"(§1)"," NICE 자격증명 재확인 + 옵션 B(Cloudflare 대역 등록) 시도 → 여전히 1007. 진단용 ",[28,124,125],{},"\u002Fdiag\u002Fegress","로 Workers의 outbound가 ",[23,128,129],{},"IPv6","(",[28,132,133],{},"2a06:98c0:3600::103",", Cloudflare ",[28,136,137],{},"2a06:98c0::\u002F29",")임을 확인. NICE 콘솔에 IPv4 대역만 등록된 게 원인 — IPv6 대역(7개) 추가 등록 안내 + mock 복귀. ",[23,140,141],{},"(§2)"," Hyperdrive 바인딩 교체 — ",[28,144,145],{},"a2ba4efe..."," → ",[28,148,149],{},"439b109d...",". 신규 Hyperdrive는 ",[23,152,153],{},"Cloudflare Tunnel(Access)"," 기반(host ",[28,156,157],{},"malgn-dev-db.apiserver.kr"," + ",[28,160,161],{},"access_client_id","). Aurora SG egress IP 화이트리스트 운영 부담 해소 — CLAUDE.md §12 TODO \"SG 갱신 운영 절차\" 항목 달성. 관련 정본 3개(API CLAUDE.md §3·§12·§8, SCALABILITY.md §6 신규 절, MIGRATION.md §1) 동기화. ",[23,164,165],{},"(§3)"," ",[23,168,169],{},"관리자단 핸드오프 풀세트"," — ",[28,172,173],{},"handoff_noti_admin"," (3,129줄 jsx)을 Vue 3 + Nuxt UI v3로 1:1 포팅. 셸(LNB 메뉴 트리 완전 재정비 + Topbar 동적 브레드크럼) · 공유 컴포넌트 14종(PageHeader\u002FSectionCard\u002FTabs\u002FSegmented\u002FFilterBar\u002FDateRange\u002FDataTable generic+slot\u002FPagination\u002FStatusBadge 자동 매핑\u002FChannelChip\u002FStatCard\u002FDrawer\u002FModal\u002FField\u002FEmptyState) · 차트 4종(Bar\u002FArea SVG path+gradient\u002FDonut stroke-dasharray\u002FProgress) · ",[23,176,177],{},"17 페이지","(대시보드·고객사·고객사 상세·계정·모니터링·발신번호·발신프로필·템플릿검수·결제·채널단가·충전쿠폰·1:1문의·FAQ·공지·통계·운영자·권한그룹·API). app.config.ts ",[28,180,181],{},"info: 'indigo'"," 매핑으로 핸드오프 indigo 강조색을 Nuxt UI semantic으로. 18 라우트 라이브 200. ",[23,184,185],{},"(§4)"," 사용자 보고 후속 — admin의 폰트 사이즈가 핸드오프와 다르다는 신고에서 ",[23,188,189],{},"두 원인 동시 발견",": (a) ",[28,192,193],{},"main.css",[28,195,196],{},"html,body{font-size:13px}","가 모든 Tailwind 토큰을 18% 축소시킴(핸드오프는 base font-size 명시 없음 = 16px), (b) 직전 turn의 cwd가 사용자단이라 ",[23,199,200],{},"사용자단 dist를 admin 프로젝트로 배포","한 상태였음(chunk 600개 \u002F GNB·메모·계약 컴포넌트 등 사용자단 자산이 admin URL에 노출). 둘 다 정정: 13px 제거 + ",[28,203,204],{},"letter-spacing: -0.01em"," 추가, admin 디렉토리에서 clean rebuild + 재배포. Pages alias ",[28,207,208],{},"8852d5da.malgn-noti-admin.pages.dev"," (chunk 96개로 정상화). ",[23,211,212],{},"(§5)"," WBS 페이지 편집 기능 — DB 미사용 \u002F R2 단일 JSON 객체 정본(",[28,215,42],{},"). API에 GET 공개 + PATCH 인증 2 라우트 + 142 task 시드. 사용자단은 임베디드 STAGES 제거 → API 비동기 로드 + 인라인 편집 모달(5 필드, 빈값=",[28,218,219],{},"null","=필드 제거). Workers Version ",[28,222,223],{},"28f3e6a8...",", Pages alias ",[28,226,227],{},"02bb58e6",". 후속(§5.9) — 목표일·완료일 ",[28,230,231],{},"YYYY.MM.DD"," 포맷 통일 + ",[28,234,235],{},"\u003Cinput type=\"date\">"," 캘린더 위젯 + API Zod regex 강제. Workers Version ",[28,238,239],{},"eb02206c...",[28,241,242],{},"98bd09e2",".",[245,246],"hr",{},[10,248,250],{"id":249},"_1-nice-ipv6-진단-옵션-b-시도-ipv4만-등록되어-여전히-1007","§1. NICE IPv6 진단 — 옵션 B 시도 → IPv4만 등록되어 여전히 1007",[14,252,254],{"id":253},"한-줄","한 줄",[19,256,257,258,261,262,265,266,269,270,275,276,278,279,130,282,285,286,285,289,285,292,285,295,285,298,285,300,303,304,307],{},"6\u002F2 §16의 1007 차단 후속. 사용자가 옵션 B(NICE 콘솔에 Cloudflare egress IP 대역 등록)를 진행했다고 알림 → 재시도 했지만 여전히 ",[28,259,260],{},"1007 허용되지 않은 IP 접근",". 진단을 위해 임시 endpoint ",[28,263,264],{},"GET \u002Fdiag\u002Fegress","를 추가해 외부 echo(",[28,267,268],{},"api.ipify.org",")로 실제 출발지 IP를 8회 캡처 → ",[23,271,272,273],{},"모두 IPv6 ",[28,274,133],{}," (Cloudflare 공식 IPv6 ",[28,277,137],{}," 소속). NICE에 등록된 대역은 IPv4뿐이라 IPv6 출발지가 거부됨. 즉시 mock 복원 + 진단 endpoint 제거(\u002Fdiag\u002Fegress → 404) + Workers 재배포. 사용자에게 NICE 콘솔에 ",[23,280,281],{},"Cloudflare IPv6 대역 7개",[28,283,284],{},"2400:cb00::\u002F32"," · ",[28,287,288],{},"2606:4700::\u002F32",[28,290,291],{},"2803:f800::\u002F32",[28,293,294],{},"2405:b500::\u002F32",[28,296,297],{},"2405:8100::\u002F32",[28,299,137],{},[28,301,302],{},"2c0f:f248::\u002F32",") 추가 등록 또는 IP 검사 OFF(옵션 A) 안내 후 사용자가 IP 보류 결정. 자격증명 3개(CLIENT_ID\u002FSECRET\u002FRETURN_URL) 보관 유지 — 정책 해결 시 ",[28,305,306],{},"wrangler secret delete NICE_MOCK"," 한 번이면 real 전환.",[14,309,311],{"id":310},"_11-자격증명-재확인","1.1 자격증명 재확인",[19,313,314,315,318,319,322,323,326],{},"Cloudflare는 secret 값을 ",[28,316,317],{},"secret list","로 노출하지 않음. 가장 확실한 방법은 사용자가 준 값으로 ",[23,320,321],{},"덮어쓰기"," — 일치하면 그대로, 다르면 정확한 값으로 교정. ",[28,324,325],{},"wrangler secret put NICE_CLIENT_ID\u002FSECRET","로 사용자 제공값 100% 일치 보장 처리.",[14,328,330],{"id":329},"_12-1007-재현-진단-endpoint","1.2 1007 재현 → 진단 endpoint",[332,333,338],"pre",{"className":334,"code":335,"language":336,"meta":337,"style":337},"language-ts shiki shiki-themes github-light github-dark","app.get('\u002Fdiag\u002Fegress', async (c) => {\n  const samples: string[] = []\n  for (let i = 0; i \u003C 8; i++) {\n    const r = await fetch('https:\u002F\u002Fapi.ipify.org?format=json', { cf: { cacheTtl: 0 } })\n    const j = await r.json()\n    samples.push(j.ip)\n  }\n  return c.json({ samples, unique: [...new Set(samples)] })\n})\n","ts","",[28,339,340,382,407,444,476,497,509,515,538],{"__ignoreMap":337},[341,342,345,349,353,355,359,362,366,369,373,376,379],"span",{"class":343,"line":344},"line",1,[341,346,348],{"class":347},"sVt8B","app.",[341,350,352],{"class":351},"sScJk","get",[341,354,130],{"class":347},[341,356,358],{"class":357},"sZZnC","'\u002Fdiag\u002Fegress'",[341,360,361],{"class":347},", ",[341,363,365],{"class":364},"szBVR","async",[341,367,368],{"class":347}," (",[341,370,372],{"class":371},"s4XuR","c",[341,374,375],{"class":347},") ",[341,377,378],{"class":364},"=>",[341,380,381],{"class":347}," {\n",[341,383,385,388,392,395,398,401,404],{"class":343,"line":384},2,[341,386,387],{"class":364},"  const",[341,389,391],{"class":390},"sj4cs"," samples",[341,393,394],{"class":364},":",[341,396,397],{"class":390}," string",[341,399,400],{"class":347},"[] ",[341,402,403],{"class":364},"=",[341,405,406],{"class":347}," []\n",[341,408,410,413,415,418,421,423,426,429,432,435,438,441],{"class":343,"line":409},3,[341,411,412],{"class":364},"  for",[341,414,368],{"class":347},[341,416,417],{"class":364},"let",[341,419,420],{"class":347}," i ",[341,422,403],{"class":364},[341,424,425],{"class":390}," 0",[341,427,428],{"class":347},"; i ",[341,430,431],{"class":364},"\u003C",[341,433,434],{"class":390}," 8",[341,436,437],{"class":347},"; i",[341,439,440],{"class":364},"++",[341,442,443],{"class":347},") {\n",[341,445,447,450,453,456,459,462,464,467,470,473],{"class":343,"line":446},4,[341,448,449],{"class":364},"    const",[341,451,452],{"class":390}," r",[341,454,455],{"class":364}," =",[341,457,458],{"class":364}," await",[341,460,461],{"class":351}," fetch",[341,463,130],{"class":347},[341,465,466],{"class":357},"'https:\u002F\u002Fapi.ipify.org?format=json'",[341,468,469],{"class":347},", { cf: { cacheTtl: ",[341,471,472],{"class":390},"0",[341,474,475],{"class":347}," } })\n",[341,477,479,481,484,486,488,491,494],{"class":343,"line":478},5,[341,480,449],{"class":364},[341,482,483],{"class":390}," j",[341,485,455],{"class":364},[341,487,458],{"class":364},[341,489,490],{"class":347}," r.",[341,492,493],{"class":351},"json",[341,495,496],{"class":347},"()\n",[341,498,500,503,506],{"class":343,"line":499},6,[341,501,502],{"class":347},"    samples.",[341,504,505],{"class":351},"push",[341,507,508],{"class":347},"(j.ip)\n",[341,510,512],{"class":343,"line":511},7,[341,513,514],{"class":347},"  }\n",[341,516,518,521,524,526,529,532,535],{"class":343,"line":517},8,[341,519,520],{"class":364},"  return",[341,522,523],{"class":347}," c.",[341,525,493],{"class":351},[341,527,528],{"class":347},"({ samples, unique: [",[341,530,531],{"class":364},"...new",[341,533,534],{"class":351}," Set",[341,536,537],{"class":347},"(samples)] })\n",[341,539,541],{"class":343,"line":540},9,[341,542,543],{"class":347},"})\n",[19,545,546,547,549,550,552],{},"연속 2회 호출 결과 — 16번 모두 동일 IP: ",[28,548,133],{},". Cloudflare 공식 IPv6 대역 ",[28,551,137],{}," 소속.",[14,554,556],{"id":555},"_13-결정-ipv6-대역-추가-등록-또는-ip-검사-off","1.3 결정 — IPv6 대역 추가 등록 또는 IP 검사 OFF",[558,559,560,573],"table",{},[561,562,563],"thead",{},[564,565,566,570],"tr",{},[567,568,569],"th",{},"옵션",[567,571,572],{},"작업",[574,575,576,585,593],"tbody",{},[564,577,578,582],{},[579,580,581],"td",{},"A. NICE 콘솔에서 IP 인증 OFF",[579,583,584],{},"콘솔 → 보안설정 토글",[564,586,587,590],{},[579,588,589],{},"B'. IPv6 대역 7개 추가 등록 (오늘 사용자가 했던 B는 IPv4만 등록되어 실패)",[579,591,592],{},"Cloudflare 공식 IPv6 목록 NICE 영업에 송부",[564,594,595,598],{},[579,596,597],{},"C. 고정 IP 프록시 EC2",[579,599,600],{},"AWS EC2 nano + 어댑터 baseUrl 변경",[19,602,603,604,607,608,611],{},"사용자 의사로 일단 ",[23,605,606],{},"보류"," 결정. ",[28,609,610],{},"NICE_MOCK=1"," 복원해 가입 흐름은 mock 정상. 진단 endpoint는 보안상 즉시 제거 + 재배포로 라이브에서 404.",[14,613,615],{"id":614},"_14-산출물","1.4 산출물",[617,618,619,623,633],"ul",{},[620,621,622],"li",{},"코드: 없음(진단 endpoint는 일시 추가 후 제거)",[620,624,625,626,629,630,632],{},"secret: ",[28,627,628],{},"NICE_CLIENT_ID\u002FSECRET"," 덮어쓰기(값 변경 없음, 일치 보장만), ",[28,631,610],{}," 유지",[620,634,635,636,639,640],{},"Workers 배포 — 진단 endpoint 추가 시 Version ",[28,637,638],{},"e77f89e0-8fed-4b27-a56a-a212d916cba3",", 제거 후 Version ",[28,641,642],{},"0e3d3eb0-38ca-487f-8a28-355b0243a5a6",[14,644,646],{"id":645},"_15-보안-메모","1.5 보안 메모",[19,648,649,652,653,656],{},[28,650,651],{},"CLIENT_SECRET","이 6\u002F2 채팅 평문 노출 + 오늘 한 번 더 노출. IP 정책 해결 시점에 NICE 콘솔에서 회전 권장. ",[28,654,655],{},"doc\u002FMEMBERSHIP.md"," §9 후속 작업 21번에 이미 등록됨.",[245,658],{},[10,660,662],{"id":661},"_2-hyperdrive-교체-cloudflare-tunnelaccess-기반-workers-version-a457b7dc","§2. Hyperdrive 교체 — Cloudflare Tunnel(Access) 기반 (Workers Version a457b7dc)",[14,664,254],{"id":665},"한-줄-1",[19,667,668,671,672,146,675,678,679,681,682,685,686,689,690,693],{},[28,669,670],{},"wrangler.toml","의 HYPERDRIVE id를 ",[28,673,674],{},"a2ba4efe7421464da1d5ff5e620b33a3",[28,676,677],{},"439b109dd219479e8b3e8d80eea9a240","으로 교체. 신규 Hyperdrive origin host가 ",[28,680,157],{}," (Cloudflare Tunnel 엔드포인트) + ",[28,683,684],{},"access_client_id: 50b64dc493c35f1a9d9916baf4e2d735.access"," (Cloudflare Access 서비스 토큰)이라, 기존 \"퍼블릭 엔드포인트 + SG inbound에 Hyperdrive egress IP 화이트리스트\" 구성이 \"Cloudflare Tunnel 기반\"으로 전환됨. 코드 변경 0 — Hyperdrive 바인딩 인터페이스 동일, mysql2 드라이버 그대로 동작. 라이브 검증 통과(",[28,687,688],{},"GET \u002Fhealth\u002Fdb"," mysql_version 8.0.42 + ",[28,691,692],{},"POST \u002Fauth\u002Fnice\u002Finit"," DB 쓰기 정상). 관련 정본 3개 문서 동기화.",[14,695,697],{"id":696},"_21-전환-효과","2.1 전환 효과",[558,699,700,713],{},[561,701,702],{},[564,703,704,707,710],{},[567,705,706],{},"항목",[567,708,709],{},"이전 (~6\u002F1)",[567,711,712],{},"신규 (6\u002F4~)",[574,714,715,726,737,748],{},[564,716,717,720,723],{},[579,718,719],{},"Aurora 노출",[579,721,722],{},"퍼블릭 엔드포인트 + SG 화이트리스트",[579,724,725],{},"Cloudflare Tunnel 뒤",[564,727,728,731,734],{},[579,729,730],{},"출발지 인증",[579,732,733],{},"Hyperdrive egress IP가 SG에 등록되어야 통과",[579,735,736],{},"Tunnel access_client_id로 인증 (출발지 IP 무관)",[564,738,739,742,745],{},[579,740,741],{},"egress IP 갱신 추적",[579,743,744],{},"Cloudflare 공식 IP 목록 변경 시 SG 동기화 필요",[579,746,747],{},"불필요",[564,749,750,757,760],{},[579,751,752,753,756],{},"로컬 ",[28,754,755],{},"drizzle-kit migrate"," 직결",[579,758,759],{},"SG 제약으로 불가 (admin 라우트로 우회)",[579,761,762],{},"Tunnel 인증 없으면 통과 불가 (동일 운영 절차 유지)",[19,764,765,766,769],{},"CLAUDE.md §12 TODO 중 \"",[23,767,768],{},"SG 갱신 운영 절차",": Hyperdrive egress IP 목록이 바뀔 때 어떻게 감지\u002F반영할지\" 항목 자연 달성 — 더 이상 필요 없음.",[14,771,773],{"id":772},"_22-라이브-검증","2.2 라이브 검증",[617,775,776,784,789],{},[620,777,778,146,780,783],{},[28,779,688],{},[28,781,782],{},"{ok: true, mysql_version: \"8.0.42\"}"," ✅",[620,785,786,788],{},[28,787,692],{}," (DB 쓰기) → mockMode true + niceAuth row insert ✅",[620,790,791,794],{},[28,792,793],{},"GET \u002Fcontracts"," (인증 라우트) → 401 ✅",[14,796,798],{"id":797},"_23-정본-문서-동기화","2.3 정본 문서 동기화",[558,800,801,811],{},[561,802,803],{},[564,804,805,808],{},[567,806,807],{},"파일",[567,809,810],{},"변경",[574,812,813,824,834,847,861],{},[564,814,815,821],{},[579,816,817,820],{},[28,818,819],{},"malgn-noti-api\u002FCLAUDE.md"," §3",[579,822,823],{},"Aurora 노출 방식을 Tunnel로 명시. 이전 SG 화이트리스트 절차 삭제. egress IP 동기화 부담 제거",[564,825,826,831],{},[579,827,828,830],{},[28,829,819],{}," §12 TODO",[579,832,833],{},"\"SG 갱신 운영 절차\" 항목 ✅ 완료 표시",[564,835,836,841],{},[579,837,838,840],{},[28,839,819],{}," §8",[579,842,843,846],{},[28,844,845],{},"pnpm db:migrate"," 안내 문구 \"Aurora SG 제약\" → \"Tunnel 뒤\"로",[564,848,849,855],{},[579,850,851,854],{},[28,852,853],{},"malgn-noti-api\u002Fdoc\u002FSCALABILITY.md"," §6",[579,856,857,860],{},[23,858,859],{},"신규 절"," \"Hyperdrive ↔ Aurora 연결 방식 — Cloudflare Tunnel(Access) 기반 (2026-06-02 이후)\" 추가. 전\u002F후 구성 표 + 전환 이유 + 운영 영향 + 라이브 검증 + 신규 Hyperdrive id 명시",[564,862,863,869],{},[579,864,865,868],{},[28,866,867],{},"malgn-noti-api\u002Fdoc\u002FMIGRATION.md"," §1",[579,870,871],{},"통로 다이어그램에 Tunnel 단계 추가, 신규 Hyperdrive id 반영",[14,873,875],{"id":874},"_24-산출물","2.4 산출물",[617,877,878],{},[620,879,880,881,884,885],{},"API: ",[28,882,883],{},"malgn-noti-api"," — 2 커밋\n",[617,886,887,896],{},[620,888,889,892,893],{},[28,890,891],{},"3d779ad"," chore(wrangler): Hyperdrive id 교체. Workers 배포 Version ",[28,894,895],{},"a457b7dc-e951-4f2a-bc78-29b5496fa90f",[620,897,898,901],{},[28,899,900],{},"334ee69"," doc(infra): Aurora 연결 방식 Cloudflare Tunnel 전환 반영 (코드 변경 없음 — 정본만)",[14,903,905],{"id":904},"_25-알려진-한계-후속","2.5 알려진 한계 \u002F 후속",[617,907,908,914,924],{},[620,909,910,913],{},[23,911,912],{},"Aurora SG inbound 단순화"," — Tunnel 전환으로 더 이상 Cloudflare egress IP 추적 불필요하므로 SG inbound는 Tunnel daemon 호스트만 허용하도록 단순화 가능. AWS 측 정리 후속.",[620,915,916,923],{},[23,917,918,919,922],{},"Aurora ",[28,920,921],{},"PubliclyAccessible=false"," 전환 검토"," — Tunnel만 노출이 되면 퍼블릭 IP 제거 가능. 운영 정책 합의 후 적용.",[620,925,926,929],{},[23,927,928],{},"Tunnel 가용성 모니터링"," — Tunnel daemon이 죽으면 전 Workers DB 호출이 실패. 인지·복구 절차(Cloudflare 대시보드 알람 + 백업 경로) 정의 후속.",[245,931],{},[10,933,935],{"id":934},"_3-관리자단-핸드오프-17-페이지-풀세트-셸-공유-컴포넌트-차트-구현-pages-alias-8852d5da","§3. 관리자단 핸드오프 — 17 페이지 풀세트 + 셸 + 공유 컴포넌트 + 차트 구현 (Pages alias 8852d5da)",[14,937,254],{"id":938},"한-줄-2",[19,940,941,942,944,945,948,949,952,953,956],{},"신규 핸드오프 정본 ",[28,943,173],{},"(prototype 3,129줄 jsx + 스타일 가이드 + 8 스크린샷)을 받아 사용자 요청으로 17 페이지 풀세트 신규 작성. 기존 admin은 셸(",[28,946,947],{},"AppLnb","\u002F",[28,950,951],{},"AppTopbar",")만 있는 부트스트랩 상태였고 LNB 메뉴 트리도 핸드오프와 완전히 달라서 (a) 셸 완전 재정비 + (b) 공유 컴포넌트 14종 + (c) 차트 4종 + (d) 17 페이지를 모두 신규로 작성. Vue 3 + Nuxt UI v3로 jsx → Vue 1:1 포팅 패턴. 라이브 18 라우트(17 + 동적 ",[28,954,955],{},"\u002Fcustomers\u002F:id",") 모두 200 검증.",[14,958,960],{"id":959},"_31-핸드오프-정본-ia","3.1 핸드오프 정본 IA",[19,962,963,964,394],{},"10 섹션 \u002F 17 라우트 — ",[28,965,966],{},"handoff_noti_admin\u002Fprototype\u002Flnb.jsx",[558,968,969,985],{},[561,970,971],{},[564,972,973,976,979,982],{},[567,974,975],{},"#",[567,977,978],{},"라우트 키",[567,980,981],{},"경로",[567,983,984],{},"화면",[574,986,987,1004,1022,1040,1058,1076,1094,1112,1130,1148,1166,1184,1202,1220,1238,1256,1274,1292],{},[564,988,989,992,997,1001],{},[579,990,991],{},"01",[579,993,994],{},[28,995,996],{},"dashboard",[579,998,999],{},[28,1000,948],{},[579,1002,1003],{},"KPI4 + 발송추이(area) + 채널비중(donut) + 검수큐 + 실시간 발송",[564,1005,1006,1009,1014,1019],{},[579,1007,1008],{},"02",[579,1010,1011],{},[28,1012,1013],{},"customer",[579,1015,1016],{},[28,1017,1018],{},"\u002Fcustomers",[579,1020,1021],{},"필터바 + 선택 + 페이지네이션",[564,1023,1024,1027,1032,1037],{},[579,1025,1026],{},"02d",[579,1028,1029],{},[28,1030,1031],{},"customer\u002F:id",[579,1033,1034],{},[28,1035,1036],{},"\u002Fcustomers\u002F[id]",[579,1038,1039],{},"InfoCard(KPI5) + 탭6 + 계정 테이블 + 메모 타임라인 + 차트 + 권한변경 모달",[564,1041,1042,1045,1050,1055],{},[579,1043,1044],{},"03",[579,1046,1047],{},[28,1048,1049],{},"account",[579,1051,1052],{},[28,1053,1054],{},"\u002Faccounts",[579,1056,1057],{},"계정 목록",[564,1059,1060,1063,1068,1073],{},[579,1061,1062],{},"04",[579,1064,1065],{},[28,1066,1067],{},"monitoring",[579,1069,1070],{},[28,1071,1072],{},"\u002Fmonitoring",[579,1074,1075],{},"LIVE KPI4 + 처리량 라인 + 작업 큐(진행바·상태배지)",[564,1077,1078,1081,1086,1091],{},[579,1079,1080],{},"05",[579,1082,1083],{},[28,1084,1085],{},"sender-num",[579,1087,1088],{},[28,1089,1090],{},"\u002Fsenders\u002Fnumbers",[579,1092,1093],{},"검수 목록 → 검수 Drawer(증빙·메모·승인\u002F반려)",[564,1095,1096,1099,1104,1109],{},[579,1097,1098],{},"06",[579,1100,1101],{},[28,1102,1103],{},"sender-profile",[579,1105,1106],{},[28,1107,1108],{},"\u002Fsenders\u002Fprofiles",[579,1110,1111],{},"동일 패턴",[564,1113,1114,1117,1122,1127],{},[579,1115,1116],{},"07",[579,1118,1119],{},[28,1120,1121],{},"template",[579,1123,1124],{},[28,1125,1126],{},"\u002Ftemplates",[579,1128,1129],{},"KPI4 + 미리보기 Drawer + 자동검수 결과",[564,1131,1132,1135,1140,1145],{},[579,1133,1134],{},"08",[579,1136,1137],{},[28,1138,1139],{},"billing",[579,1141,1142],{},[28,1143,1144],{},"\u002Fbilling",[579,1146,1147],{},"KPI4 + 충전\u002F차감\u002F환불 내역",[564,1149,1150,1153,1158,1163],{},[579,1151,1152],{},"09",[579,1154,1155],{},[28,1156,1157],{},"price-channel",[579,1159,1160],{},[28,1161,1162],{},"\u002Fpricing\u002Fchannels",[579,1164,1165],{},"채널별 단가표",[564,1167,1168,1171,1176,1181],{},[579,1169,1170],{},"10",[579,1172,1173],{},[28,1174,1175],{},"price-charge",[579,1177,1178],{},[28,1179,1180],{},"\u002Fpricing\u002Fcoupons",[579,1182,1183],{},"Tier 보너스 + 쿠폰\u002F프로모션",[564,1185,1186,1189,1194,1199],{},[579,1187,1188],{},"11",[579,1190,1191],{},[28,1192,1193],{},"inquiry",[579,1195,1196],{},[28,1197,1198],{},"\u002Fsupport\u002Finquiries",[579,1200,1201],{},"답변 Drawer",[564,1203,1204,1207,1212,1217],{},[579,1205,1206],{},"12",[579,1208,1209],{},[28,1210,1211],{},"faq",[579,1213,1214],{},[28,1215,1216],{},"\u002Fsupport\u002Ffaq",[579,1218,1219],{},"카테고리 사이드 + FAQ 목록",[564,1221,1222,1225,1230,1235],{},[579,1223,1224],{},"13",[579,1226,1227],{},[28,1228,1229],{},"notice",[579,1231,1232],{},[28,1233,1234],{},"\u002Fsupport\u002Fnotices",[579,1236,1237],{},"공지 목록(고정·분류·공개)",[564,1239,1240,1243,1248,1253],{},[579,1241,1242],{},"14",[579,1244,1245],{},[28,1246,1247],{},"report",[579,1249,1250],{},[28,1251,1252],{},"\u002Freports",[579,1254,1255],{},"KPI4 + 월별 추이 + 실패사유 donut + 채널 bar + Top 고객사",[564,1257,1258,1261,1266,1271],{},[579,1259,1260],{},"15",[579,1262,1263],{},[28,1264,1265],{},"sys-operator",[579,1267,1268],{},[28,1269,1270],{},"\u002Fsystem\u002Foperators",[579,1272,1273],{},"목록 + 운영자 추가 모달",[564,1275,1276,1279,1284,1289],{},[579,1277,1278],{},"16",[579,1280,1281],{},[28,1282,1283],{},"sys-role",[579,1285,1286],{},[28,1287,1288],{},"\u002Fsystem\u002Froles",[579,1290,1291],{},"권한 그룹 카드 + 권한 매트릭스",[564,1293,1294,1297,1302,1307],{},[579,1295,1296],{},"17",[579,1298,1299],{},[28,1300,1301],{},"sys-api",[579,1303,1304],{},[28,1305,1306],{},"\u002Fsystem\u002Fapi",[579,1308,1309],{},"KPI4 + API 키 + 웹훅",[14,1311,1313],{"id":1312},"_32-셸-재정비","3.2 셸 재정비",[617,1315,1316,1332,1346,1358],{},[620,1317,1318,1323,1324,1327,1328,1331],{},[23,1319,1320,1322],{},[28,1321,947],{}," 완전 재작성"," — 기존 메뉴 트리(8 그룹\u002F예약발송\u002FFCM\u002FAPNs 등)를 폐기하고 핸드오프 정본 9 그룹 \u002F 17 라우트로 교체. ",[28,1325,1326],{},"NuxtLink"," 기반 라우트 경로 매핑 + ",[28,1329,1330],{},"isActive()"," (path prefix 매칭) + 메뉴 검색 입력 + AI 발송 도우미 배너 + 사용자 칩.",[620,1333,1334,170,1338,1341,1342,1345],{},[23,1335,1336],{},[28,1337,951],{},[28,1339,1340],{},"useState('breadcrumb')"," 기반 동적 브레드크럼으로 변경. 페이지에서 ",[28,1343,1344],{},"useBreadcrumb(['회원\u002F고객사', '고객사'])"," 호출로 즉시 갱신.",[620,1347,1348,1354,1355,243],{},[23,1349,1350,1353],{},[28,1351,1352],{},"useBreadcrumb"," composable"," 신규 — ",[28,1356,1357],{},"composables\u002FuseBreadcrumb.ts",[620,1359,1360,170,1365,1367,1368,1371,1372,1375],{},[23,1361,1362],{},[28,1363,1364],{},"app.config.ts",[28,1366,181],{}," 매핑 추가. 핸드오프의 indigo 강조색(",[28,1369,1370],{},"검수자"," 권한, ",[28,1373,1374],{},"PUSH"," 채널 등)을 Nuxt UI v3 semantic color로 사용 가능하게.",[14,1377,1379],{"id":1378},"_33-공유-컴포넌트-14종","3.3 공유 컴포넌트 14종",[558,1381,1382,1392],{},[561,1383,1384],{},[564,1385,1386,1389],{},[567,1387,1388],{},"컴포넌트",[567,1390,1391],{},"핵심",[574,1393,1394,1404,1414,1434,1444,1458,1468,1485,1495,1508,1518,1528,1538,1548,1558],{},[564,1395,1396,1401],{},[579,1397,1398],{},[28,1399,1400],{},"AppPageHeader",[579,1402,1403],{},"caption · title · badges slot · actions slot",[564,1405,1406,1411],{},[579,1407,1408],{},[28,1409,1410],{},"AppSectionCard",[579,1412,1413],{},"title\u002Fsubtitle\u002Factions\u002FnoBody\u002FbodyClass",[564,1415,1416,1421],{},[579,1417,1418],{},[28,1419,1420],{},"AppTabs",[579,1422,1423,158,1426,1429,1430,1433],{},[28,1424,1425],{},"tabs[]",[28,1427,1428],{},"v-model"," (string|",[28,1431,1432],{},"{value,label}",")",[564,1435,1436,1441],{},[579,1437,1438],{},[28,1439,1440],{},"AppSegmented",[579,1442,1443],{},"pill 단일선택 (같은 패턴)",[564,1445,1446,1451],{},[579,1447,1448],{},[28,1449,1450],{},"AppFilterBar",[579,1452,1453,1454,1457],{},"2열 그리드 + 조회\u002F초기화. 필드는 ",[28,1455,1456],{},"#field-N"," slot으로",[564,1459,1460,1465],{},[579,1461,1462],{},[28,1463,1464],{},"AppDateRange",[579,1466,1467],{},"from\u002Fto 표시 (placeholder)",[564,1469,1470,1475],{},[579,1471,1472],{},[28,1473,1474],{},"AppDataTable\u003CT>",[579,1476,1477,1484],{},[23,1478,1479,1480,1483],{},"generic + ",[28,1481,1482],{},"#cell-{key}"," scoped slot"," 패턴. selectable + 선택 set + row-click + min-width + 빈 상태",[564,1486,1487,1492],{},[579,1488,1489],{},[28,1490,1491],{},"AppPagination",[579,1493,1494],{},"page navigation + page size select",[564,1496,1497,1502],{},[579,1498,1499],{},[28,1500,1501],{},"AppStatusBadge",[579,1503,1504,1507],{},[23,1505,1506],{},"값→톤 자동 매핑"," (활성\u002F완료=success · 대기\u002F검수중=warning · 중지\u002F반려=error · 임시저장=neutral). 핸드오프 README §7 매핑 그대로",[564,1509,1510,1515],{},[579,1511,1512],{},[28,1513,1514],{},"AppChannelChip",[579,1516,1517],{},"PUSH=indigo · RCS=primary · SMS=emerald · 알림톡=amber · 이메일=sky",[564,1519,1520,1525],{},[579,1521,1522],{},[28,1523,1524],{},"AppStatCard",[579,1526,1527],{},"icon + label + value + sub + delta(deltaUp 분기) + accent 7종",[564,1529,1530,1535],{},[579,1531,1532],{},[28,1533,1534],{},"AppDrawer",[579,1536,1537],{},"Teleport + body scroll lock + footer slot",[564,1539,1540,1545],{},[579,1541,1542],{},[28,1543,1544],{},"AppModal",[579,1546,1547],{},"중앙 + Teleport + scroll lock",[564,1549,1550,1555],{},[579,1551,1552],{},[28,1553,1554],{},"AppField",[579,1556,1557],{},"label + required + hint",[564,1559,1560,1565],{},[579,1561,1562],{},[28,1563,1564],{},"AppEmptyState",[579,1566,1567],{},"icon + title + desc + action slot",[19,1569,1570,1573,1574,1577,1578,1581],{},[28,1571,1572],{},"AppDataTable","의 generic + scoped slot 패턴이 핸드오프 prototype의 ",[28,1575,1576],{},"columns: [{render: r => JSX}]"," 패턴을 Vue로 가장 자연스럽게 옮긴 결과. 페이지에서 ",[28,1579,1580],{},"\u003Ctemplate #cell-status=\"{ row }\">\u003CAppStatusBadge :value=\"row.status\" \u002F>\u003C\u002Ftemplate>"," 식으로 컬럼별 렌더 정의.",[14,1583,1585],{"id":1584},"_34-차트-컴포넌트-4종","3.4 차트 컴포넌트 4종",[19,1587,1588,1589,1592],{},"핸드오프 ",[28,1590,1591],{},"charts.jsx","(111줄, SVG 기반)와 동등:",[617,1594,1595,1601,1607,1616],{},[620,1596,1597,1600],{},[28,1598,1599],{},"AppBarChart"," — 그룹\u002F단일 막대 (div + height) + highlight + 점선 가이드",[620,1602,1603,1606],{},[28,1604,1605],{},"AppAreaChart"," — SVG path(라인 + 영역 + 그라데이션) + 도트 + 라벨 축",[620,1608,1609,170,1612,1615],{},[28,1610,1611],{},"AppDonut",[28,1613,1614],{},"stroke-dasharray","로 segment + center 라벨\u002Fsub + 우측 범례",[620,1617,1618,1621],{},[28,1619,1620],{},"AppProgressBar"," — 가로 진행바 + show-pct 옵션",[19,1623,1624],{},"외부 차트 라이브러리 없이 동작(prototype 그대로). 후속에서 ApexCharts\u002FChart.js로 교체 가능.",[14,1626,1628],{"id":1627},"_35-17-페이지","3.5 17 페이지",[19,1630,1631,1632,158,1635,1638,1639,948,1641,948,1643,948,1645,948,1647,1649],{},"각 페이지에서 ",[28,1633,1634],{},"useHead({title})",[28,1636,1637],{},"useBreadcrumb([...])"," 호출 + 더미 데이터 ref + ",[28,1640,1400],{},[28,1642,1450],{},[28,1644,1410],{},[28,1646,1572],{},[28,1648,1491],{}," 조합 + Drawer\u002FModal(필요 시).",[19,1651,1652,1653,1656],{},"가장 큰 페이지: ",[28,1654,1655],{},"customers\u002F[id]"," (~370 라인) — InfoCard(좌측 identity + KPI5) + 탭 + 필터 + 계정 테이블(선택·역할 컬러·채널 칩) + 차트 + 최근 활동 + 메모 패널(우측 sticky aside) + 권한변경 모달(3열 라디오 그리드).",[14,1658,1660],{"id":1659},"_36-라이브-검증","3.6 라이브 검증",[332,1662,1667],{"className":1663,"code":1665,"language":1666},[1664],"language-text","\u002F                       200    \u002Fsupport\u002Finquiries     200\n\u002Fcustomers              200    \u002Fsupport\u002Ffaq           200\n\u002Fcustomers\u002FC2241        200    \u002Fsupport\u002Fnotices       200\n\u002Faccounts               200    \u002Freports               200\n\u002Fmonitoring             200    \u002Fsystem\u002Foperators      200\n\u002Fsenders\u002Fnumbers        200    \u002Fsystem\u002Froles          200\n\u002Fsenders\u002Fprofiles       200    \u002Fsystem\u002Fapi            200\n\u002Ftemplates              200\n\u002Fbilling                200\n\u002Fpricing\u002Fchannels       200\n\u002Fpricing\u002Fcoupons        200\n","text",[28,1668,1665],{"__ignoreMap":337},[19,1670,1671],{},"18 라우트 모두 정상.",[14,1673,1675],{"id":1674},"_37-산출물","3.7 산출물",[617,1677,1678],{},[620,1679,1680,1683,1684,1687],{},[28,1681,1682],{},"malgn-noti-admin: 0227cae"," — 21 컴포넌트 + 1 composable + 17 페이지(폴더 8개) + app.config.ts + AppLnb\u002FAppTopbar 갱신. Pages 초기 배포 alias ",[28,1685,1686],{},"82178863.malgn-noti-admin.pages.dev"," (이 배포는 §4의 사용자단 잘못 배포 시점에 덮어써짐 — §4 참조)",[14,1689,1691],{"id":1690},"_38-알려진-한계-후속","3.8 알려진 한계 \u002F 후속",[617,1693,1694,1715,1721,1727,1733],{},[620,1695,1696,1699,1700,1702,1703,1706,1707,1710,1711,1714],{},[23,1697,1698],{},"실 API 연동"," — 현재 모든 더미 데이터. ",[28,1701,883],{},"의 ",[28,1704,1705],{},"\u002Fadmin\u002F*"," 라우트 (대부분 미구현)를 신설 후 교체. ",[28,1708,1709],{},"\u002Fadmin\u002Fcompanies","·",[28,1712,1713],{},"\u002Fadmin\u002Fcompanies\u002F:id\u002F{approve,reject}","가 P0(승인 게이트의 짝).",[620,1716,1717,1720],{},[23,1718,1719],{},"운영자 인증·RBAC 미들웨어"," — admin CLAUDE.md §4 보안 원칙(2FA, Cloudflare Access, RBAC). 현재는 공개 라이브.",[620,1722,1723,1726],{},[23,1724,1725],{},"반응형 보강"," — 핸드오프는 1600px 데스크톱 기준. 1280px 미만 메모 패널 숨김, 1024px 미만 LNB drawer화 필요.",[620,1728,1729,1732],{},[23,1730,1731],{},"차트 라이브러리 도입"," — 현재 SVG 자체 구현(prototype 그대로). 데이터 양이 늘면 ApexCharts\u002FChart.js로 교체 검토.",[620,1734,1735,1738],{},[23,1736,1737],{},"고객사 상세 메모 composer 동작"," — 현재는 placeholder 입력만 있고 등록 동작 없음. 운영자단 메모 API와 함께 후속.",[245,1740],{},[10,1742,1744],{"id":1743},"_4-폰트-사이즈-정합화-base-16px-복원-사용자단을-admin에-잘못-배포한-사고-정정-pages-alias-8852d5da","§4. 폰트 사이즈 정합화 — base 16px 복원 + 사용자단을 admin에 잘못 배포한 사고 정정 (Pages alias 8852d5da)",[14,1746,254],{"id":1747},"한-줄-3",[19,1749,1750,1751,1754,1755,170,1758,1761,1762,1765,1766,1769,1770,1773,1774,158,1777,1780,1781,1784,1785,948,1788,948,1791,1794,1795,1797,1798,1800],{},"사용자 보고 — \"관리자단의 폰트 사이즈가 핸드오프 디자인과 다르다.\" 진단 결과 ",[23,1752,1753],{},"두 원인이 겹쳐"," 있었음. ",[23,1756,1757],{},"(a) main.css의 base font-size 13px",[28,1759,1760],{},"html, body { font-size: 13px }","로 명시되어 있어 Tailwind 토큰(",[28,1763,1764],{},"text-xs=0.75rem"," 등은 16px base 가정)이 모두 약 18% 작게 표시됨. 핸드오프 prototype\u002Findex.html과 스타일 가이드는 body에 font-size 명시 없음(= 16px Tailwind 기본). ",[23,1767,1768],{},"(b) 사용자단을 admin에 잘못 배포"," — 직전 turn의 cwd가 ",[28,1771,1772],{},"\u002FUsers\u002Fdotype\u002FProjects\u002Fmalgn-noti","(사용자단)였고, 거기서 ",[28,1775,1776],{},"pnpm build",[28,1778,1779],{},"wrangler pages deploy dist --project-name=malgn-noti-admin","을 실행한 결과 ",[23,1782,1783],{},"사용자단의 dist를 admin 프로젝트로 배포","한 상태였음. admin URL을 열어도 사용자단의 LNB·계약·메모 등 컴포넌트와 CSS 토큰(",[28,1786,1787],{},"--paper",[28,1789,1790],{},"--ink-700",[28,1792,1793],{},"--font-base",")이 떴고, 그래서 폰트 토큰도 사용자단의 것이 적용되어 핸드오프와 일치하지 않게 보임. 둘 다 정정 — (a) main.css 13px 제거 + ",[28,1796,204],{}," 추가, (b) admin 디렉토리에서 clean rebuild + 재배포(",[28,1799,208],{},"). chunk 600개(사용자단) → 96개(admin 정상)로 정상화.",[14,1802,1804],{"id":1803},"_41-a-maincss-13px-16px-정합화","4.1 (a) main.css 13px → 16px 정합화",[19,1806,1807,1808,1811],{},"핸드오프 정본 인용 (",[28,1809,1810],{},"prototype\u002Findex.html","):",[332,1813,1817],{"className":1814,"code":1815,"language":1816,"meta":337,"style":337},"language-css shiki shiki-themes github-light github-dark","html, body { font-family: \"DM Sans\",\"Pretendard Variable\",Pretendard,system-ui,sans-serif; letter-spacing: -0.01em; }\n","css",[28,1818,1819],{"__ignoreMap":337},[341,1820,1821,1825,1827,1830,1833,1836,1839,1842,1845,1848,1851,1854,1856,1859,1862,1865,1867,1870,1873],{"class":343,"line":344},[341,1822,1824],{"class":1823},"s9eBZ","html",[341,1826,361],{"class":347},[341,1828,1829],{"class":1823},"body",[341,1831,1832],{"class":347}," { ",[341,1834,1835],{"class":390},"font-family",[341,1837,1838],{"class":347},": ",[341,1840,1841],{"class":357},"\"DM Sans\"",[341,1843,1844],{"class":347},",",[341,1846,1847],{"class":357},"\"Pretendard Variable\"",[341,1849,1850],{"class":347},",Pretendard,",[341,1852,1853],{"class":390},"system-ui",[341,1855,1844],{"class":347},[341,1857,1858],{"class":390},"sans-serif",[341,1860,1861],{"class":347},"; ",[341,1863,1864],{"class":390},"letter-spacing",[341,1866,1838],{"class":347},[341,1868,1869],{"class":390},"-0.01",[341,1871,1872],{"class":364},"em",[341,1874,1875],{"class":347},"; }\n",[19,1877,1878],{},"font-size 명시 없음 → 브라우저\u002FTailwind 기본 16px base.",[19,1880,1881],{},"우리 main.css는:",[332,1883,1885],{"className":1814,"code":1884,"language":1816,"meta":337,"style":337},"html, body {\n  font-size: 13px;\n  line-height: 1.55;\n  ...\n}\n",[28,1886,1887,1897,1912,1924,1929],{"__ignoreMap":337},[341,1888,1889,1891,1893,1895],{"class":343,"line":344},[341,1890,1824],{"class":1823},[341,1892,361],{"class":347},[341,1894,1829],{"class":1823},[341,1896,381],{"class":347},[341,1898,1899,1902,1904,1906,1909],{"class":343,"line":384},[341,1900,1901],{"class":390},"  font-size",[341,1903,1838],{"class":347},[341,1905,1224],{"class":390},[341,1907,1908],{"class":364},"px",[341,1910,1911],{"class":347},";\n",[341,1913,1914,1917,1919,1922],{"class":343,"line":409},[341,1915,1916],{"class":390},"  line-height",[341,1918,1838],{"class":347},[341,1920,1921],{"class":390},"1.55",[341,1923,1911],{"class":347},[341,1925,1926],{"class":343,"line":446},[341,1927,1928],{"class":347},"  ...\n",[341,1930,1931],{"class":343,"line":478},[341,1932,1933],{"class":347},"}\n",[19,1935,1936],{},"13px base에서 Tailwind 토큰은:",[617,1938,1939,1945,1951,1957],{},[620,1940,1941,1944],{},[28,1942,1943],{},"text-xs"," (0.75rem) → 9.75px (핸드오프 12px ❌)",[620,1946,1947,1950],{},[28,1948,1949],{},"text-sm"," (0.875rem) → 11.375px (14px ❌)",[620,1952,1953,1956],{},[28,1954,1955],{},"text-base"," (1rem) → 13px (16px ❌)",[620,1958,1959,1962],{},[28,1960,1961],{},"text-2xl"," (1.5rem) → 19.5px (24px ❌)",[19,1964,1965,1966,1969],{},"→ 모든 사이즈가 약 ",[23,1967,1968],{},"18% 축소",". 페이지 제목·KPI 대표값·필터 라벨 등 전 화면에 영향.",[19,1971,1972],{},"수정:",[332,1974,1976],{"className":1814,"code":1975,"language":1816,"meta":337,"style":337},"html, body {\n  font-family: var(--font-sans);\n  letter-spacing: -0.01em;  \u002F* 핸드오프 정본 — 전역 자간 -1% *\u002F\n  ...\n}\n",[28,1977,1978,1988,2006,2024,2028],{"__ignoreMap":337},[341,1979,1980,1982,1984,1986],{"class":343,"line":344},[341,1981,1824],{"class":1823},[341,1983,361],{"class":347},[341,1985,1829],{"class":1823},[341,1987,381],{"class":347},[341,1989,1990,1993,1995,1998,2000,2003],{"class":343,"line":384},[341,1991,1992],{"class":390},"  font-family",[341,1994,1838],{"class":347},[341,1996,1997],{"class":390},"var",[341,1999,130],{"class":347},[341,2001,2002],{"class":371},"--font-sans",[341,2004,2005],{"class":347},");\n",[341,2007,2008,2011,2013,2015,2017,2020],{"class":343,"line":409},[341,2009,2010],{"class":390},"  letter-spacing",[341,2012,1838],{"class":347},[341,2014,1869],{"class":390},[341,2016,1872],{"class":364},[341,2018,2019],{"class":347},";  ",[341,2021,2023],{"class":2022},"sJ8bj","\u002F* 핸드오프 정본 — 전역 자간 -1% *\u002F\n",[341,2025,2026],{"class":343,"line":446},[341,2027,1928],{"class":347},[341,2029,2030],{"class":343,"line":478},[341,2031,1933],{"class":347},[19,2033,2034,158,2037,2040,2041,2044,2045,2047,2048,2051,2052,2054,2055,2057,2058,2061,2062,2064],{},[28,2035,2036],{},"font-size: 13px",[28,2038,2039],{},"line-height: 1.55"," 제거. Tailwind 기본 16px base 동작. 핸드오프 README §8 Typography 토큰(",[28,2042,2043],{},"text-[11px]","=11 · ",[28,2046,1943],{},"=12 · ",[28,2049,2050],{},"text-[13px]","=13 · ",[28,2053,1949],{},"=14 · ",[28,2056,1955],{},"=16 · ",[28,2059,2060],{},"text-lg","=18 · ",[28,2063,1961],{},"=24)이 정본 그대로 매칭.",[14,2066,2068],{"id":2067},"_42-b-사용자단을-admin에-잘못-배포한-사고","4.2 (b) 사용자단을 admin에 잘못 배포한 사고",[19,2070,2071,2072,2075],{},"증상 — 빌드된 admin ",[28,2073,2074],{},"entry.css","에 사용자단 토큰이 보임:",[332,2077,2079],{"className":1814,"code":2078,"language":1816,"meta":337,"style":337},"body, html { background: var(--paper); color: var(--ink-700); font-family: var(--font-sans); font-size: var(--font-base); ...; font-feature-settings: \"cv11\",\"ss01\",\"ss03\"; letter-spacing: 0; line-height: 1.55; ... }\n",[28,2080,2081],{"__ignoreMap":337},[341,2082,2083,2085,2087,2089,2091,2094,2096,2098,2100,2102,2105,2108,2110,2112,2114,2116,2118,2120,2122,2124,2126,2128,2130,2133,2135,2137,2139,2141,2144,2147,2149,2152,2154,2157,2159,2162,2164,2166,2168,2170,2172,2175,2177,2179],{"class":343,"line":344},[341,2084,1829],{"class":1823},[341,2086,361],{"class":347},[341,2088,1824],{"class":1823},[341,2090,1832],{"class":347},[341,2092,2093],{"class":390},"background",[341,2095,1838],{"class":347},[341,2097,1997],{"class":390},[341,2099,130],{"class":347},[341,2101,1787],{"class":371},[341,2103,2104],{"class":347},"); ",[341,2106,2107],{"class":390},"color",[341,2109,1838],{"class":347},[341,2111,1997],{"class":390},[341,2113,130],{"class":347},[341,2115,1790],{"class":371},[341,2117,2104],{"class":347},[341,2119,1835],{"class":390},[341,2121,1838],{"class":347},[341,2123,1997],{"class":390},[341,2125,130],{"class":347},[341,2127,2002],{"class":371},[341,2129,2104],{"class":347},[341,2131,2132],{"class":390},"font-size",[341,2134,1838],{"class":347},[341,2136,1997],{"class":390},[341,2138,130],{"class":347},[341,2140,1793],{"class":371},[341,2142,2143],{"class":347},"); ...; ",[341,2145,2146],{"class":390},"font-feature-settings",[341,2148,1838],{"class":347},[341,2150,2151],{"class":357},"\"cv11\"",[341,2153,1844],{"class":347},[341,2155,2156],{"class":357},"\"ss01\"",[341,2158,1844],{"class":347},[341,2160,2161],{"class":357},"\"ss03\"",[341,2163,1861],{"class":347},[341,2165,1864],{"class":390},[341,2167,1838],{"class":347},[341,2169,472],{"class":390},[341,2171,1861],{"class":347},[341,2173,2174],{"class":390},"line-height",[341,2176,1838],{"class":347},[341,2178,1921],{"class":390},[341,2180,2181],{"class":347},"; ... }\n",[19,2183,2184,948,2186,948,2188,948,2190,948,2193,948,2196,2199,2200,2203],{},[28,2185,1787],{},[28,2187,1790],{},[28,2189,1793],{},[28,2191,2192],{},"cv11",[28,2194,2195],{},"ss01",[28,2197,2198],{},"ss03","은 모두 사용자단(malgn-noti)의 ",[28,2201,2202],{},"app\u002Fassets\u002Fcss\u002Fmain.css"," 토큰. admin에는 정의 없음.",[19,2205,2206,2207,2210,2211,1710,2214,1710,2217,2220,2221,2224],{},"원인 — ",[28,2208,2209],{},"dist\u002F_worker.js\u002Fchunks\u002Fbuild\u002F","에 ",[28,2212,2213],{},"AppGnb-styles.*.mjs",[28,2215,2216],{},"AppContractPanel-styles.*.mjs",[28,2218,2219],{},"AppCardAddDialog-styles.*.mjs"," 등 사용자단 컴포넌트 chunk가 들어있음. chunk 총수도 ",[23,2222,2223],{},"600개","(admin 단독이면 ~96개).",[19,2226,2227,2228,2231,2232,2234,2235,2238,2239,2241,2242,2245,2246,2249],{},"추적 — ",[28,2229,2230],{},"pwd","가 ",[28,2233,1772],{},"(사용자단). 거기서 빌드한 ",[28,2236,2237],{},"dist","를 ",[28,2240,1779],{},"으로 ",[23,2243,2244],{},"다른 프로젝트로 deploy",". ",[28,2247,2248],{},"wrangler","는 dist의 출처를 검증하지 않음. project-name만 일치하면 그대로 푸시.",[19,2251,2252],{},"정정:",[332,2254,2258],{"className":2255,"code":2256,"language":2257,"meta":337,"style":337},"language-bash shiki shiki-themes github-light github-dark","cd \u002FUsers\u002Fdotype\u002FProjects\u002Fmalgn-noti-admin\nrm -rf .nuxt .output dist\npnpm build\nnpx wrangler@4 pages deploy dist --project-name=malgn-noti-admin --branch=main --commit-dirty=true --commit-message \"admin clean rebuild\"\n","bash",[28,2259,2260,2268,2285,2293],{"__ignoreMap":337},[341,2261,2262,2265],{"class":343,"line":344},[341,2263,2264],{"class":390},"cd",[341,2266,2267],{"class":357}," \u002FUsers\u002Fdotype\u002FProjects\u002Fmalgn-noti-admin\n",[341,2269,2270,2273,2276,2279,2282],{"class":343,"line":384},[341,2271,2272],{"class":351},"rm",[341,2274,2275],{"class":390}," -rf",[341,2277,2278],{"class":357}," .nuxt",[341,2280,2281],{"class":357}," .output",[341,2283,2284],{"class":357}," dist\n",[341,2286,2287,2290],{"class":343,"line":409},[341,2288,2289],{"class":351},"pnpm",[341,2291,2292],{"class":357}," build\n",[341,2294,2295,2298,2301,2304,2307,2310,2313,2316,2319,2322],{"class":343,"line":446},[341,2296,2297],{"class":351},"npx",[341,2299,2300],{"class":357}," wrangler@4",[341,2302,2303],{"class":357}," pages",[341,2305,2306],{"class":357}," deploy",[341,2308,2309],{"class":357}," dist",[341,2311,2312],{"class":390}," --project-name=malgn-noti-admin",[341,2314,2315],{"class":390}," --branch=main",[341,2317,2318],{"class":390}," --commit-dirty=true",[341,2320,2321],{"class":390}," --commit-message",[341,2323,2324],{"class":357}," \"admin clean rebuild\"\n",[19,2326,2327],{},"검증:",[617,2329,2330,2336,2345],{},[620,2331,2332,2333],{},"chunk 수 600 → ",[23,2334,2335],{},"96",[620,2337,2338,2340,2341,2344],{},[28,2339,2074],{}," body 룰: ",[28,2342,2343],{},"body,html{color:#0f172a;font-family:var(--font-sans);letter-spacing:-.01em;...}"," — font-size 없음 + letter-spacing -0.01em 정확",[620,2346,2347,2348],{},"18 라우트 200, 제목 ",[28,2349,2350],{},"대시보드 · 맑은 메시징 Admin",[14,2352,2354],{"id":2353},"_43-산출물","4.3 산출물",[617,2356,2357,2364],{},[620,2358,2359,2360,2363],{},"사용자단: 없음(사용자단은 6\u002F2 alias ",[28,2361,2362],{},"3ee66d7c"," 그대로 라이브 유지 — 이번 잘못 배포는 admin 프로젝트로만 갔으므로 사용자단 영향 없음)",[620,2365,2366,2367,2370,2371,2373],{},"관리자단: ",[28,2368,2369],{},"malgn-noti-admin: 1b63200"," fix(font) main.css 정합화. Pages 배포 alias ",[28,2372,208],{}," (clean rebuild)",[14,2375,2377],{"id":2376},"_44-교훈-운영-절차-보강-검토","4.4 교훈 \u002F 운영 절차 보강 검토",[2379,2380,2381,2428,2447],"ol",{},[620,2382,2383,2386,2387,2390,2391],{},[23,2384,2385],{},"deploy 명령 보호"," — 멀티 레포 환경에서 cwd가 잘못된 상태로 ",[28,2388,2389],{},"wrangler pages deploy","가 다른 프로젝트로 가는 사고는 재발 가능. 방어책:\n",[617,2392,2393,2409,2418],{},[620,2394,2395,170,2398,1702,2401,2404,2405,2408],{},[23,2396,2397],{},"prebuild 가드",[28,2399,2400],{},"package.json",[28,2402,2403],{},"build"," 스크립트에 ",[28,2406,2407],{},"node -e \"if (require('.\u002Fpackage.json').name !== '\u003Cexpected>') process.exit(1)\""," 사전 체크",[620,2410,2411,170,2414,2417],{},[23,2412,2413],{},"wrangler.toml의 pages 프로젝트 매칭",[28,2415,2416],{},"pnpm run deploy:pages"," 같은 알리아스 스크립트로 cwd + project-name을 묶음",[620,2419,2420,2423,2424,2427],{},[23,2421,2422],{},"이력 추적 단순화"," — 매 배포 직후 ",[28,2425,2426],{},"wrangler deployments list \u003Cproject>"," 결과의 commit hash가 expected repo HEAD와 일치하는지 확인",[620,2429,2430,2433,2434,948,2436,2210,2438,2440,2441,1702,2444,2446],{},[23,2431,2432],{},"base font-size 명시 패턴 금지"," — Tailwind 토큰이 16px base를 가정하므로, ",[28,2435,1824],{},[28,2437,1829],{},[28,2439,2132],{},"를 다른 값으로 명시하면 모든 토큰이 어긋남. 필요한 경우엔 토큰 자체(",[28,2442,2443],{},"@theme",[28,2445,1793],{},")를 갱신.",[620,2448,2449,2452],{},[23,2450,2451],{},"chunk 수 sanity check"," — admin 같이 17 페이지 규모면 chunk가 ~100개 안팎. 600개가 떴다면 외부 자산 혼입 의심.",[245,2454],{},[10,2456,2458],{"id":2457},"_5-wbs-페이지-편집-기능-r2-json-정본-인라인-모달-workers-version-28f3e6a8-pages-alias-02bb58e6","§5. WBS 페이지 편집 기능 — R2 JSON 정본 + 인라인 모달 (Workers Version 28f3e6a8 \u002F Pages alias 02bb58e6)",[14,2460,2462],{"id":2461},"_51-배경","5.1 배경",[19,2464,2465,2467,2468,2475,2476,2479,2480,2483],{},[28,2466,50],{}," 페이지는 그동안 ",[2469,2470,2472],"a",{"href":2471},"..\u002F..\u002Fapp\u002Fpages\u002Fwbs.vue",[28,2473,2474],{},"app\u002Fpages\u002Fwbs.vue"," 안에 STAGES 상수로 임베디드된 데이터 — 매번 코드 수정 + 배포해야 진척률·메모를 갱신할 수 있었다. 사용자가 \"",[23,2477,2478],{},"설명·링크·목표일·완료일·담당자를 수정할 수 있게","\" + \"",[23,2481,2482],{},"DB 미사용, R2에 JSON 파일로 저장","\" 정책을 지정.",[14,2485,2487],{"id":2486},"_52-결정","5.2 결정",[617,2489,2490,2503,2524,2538],{},[620,2491,2492,2495,2496,2499,2500,2502],{},[23,2493,2494],{},"저장소",": R2 단일 객체 (",[28,2497,2498],{},"malgn-noti-files"," 버킷 \u002F 키 ",[28,2501,42],{},"). 기존 FILES 바인딩 재사용 — 신규 바인딩 없음.",[620,2504,2505,1838,2508,285,2511,285,2514,285,2517,285,2520,2523],{},[23,2506,2507],{},"편집 가능 필드 5개",[28,2509,2510],{},"note",[28,2512,2513],{},"href",[28,2515,2516],{},"targetDate",[28,2518,2519],{},"completionDate",[28,2521,2522],{},"owner",". 상태(완료\u002F진행 중\u002F대기) · 단계 가중치 · 진행률 · 그룹 · 제목은 본 화면 편집 대상 아님(시드\u002F코드 변경).",[620,2525,2526,2529,2530,2533,2534,2537],{},[23,2527,2528],{},"인증 정책",": GET 공개 \u002F PATCH 로그인 필요. 페이지 자체는 그대로 ",[28,2531,2532],{},"auth: false",", 편집 버튼이 ",[28,2535,2536],{},"auth.user","일 때만 노출.",[620,2539,2540,2543],{},[23,2541,2542],{},"동시성",": last-write-wins. 단일 운영자 저빈도 사용 가정. ETag\u002FIf-Match는 향후 도입 여지.",[14,2545,2547],{"id":2546},"_53-api-변경-malgn-noti-api","5.3 API 변경 (malgn-noti-api)",[19,2549,2550],{},"신규 파일:",[617,2552,2553,2566],{},[620,2554,2555,2561,2562,2565],{},[2469,2556,2558],{"href":2557},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Fdata\u002Fwbs-seed.ts",[28,2559,2560],{},"src\u002Fdata\u002Fwbs-seed.ts"," — 5 stages \u002F ",[23,2563,2564],{},"142 tasks"," 시드. 현행 사용자단 임베디드 STAGES 그대로 복제. R2 미존재 시 첫 GET이 이 값을 PUT 후 반환.",[620,2567,2568,2574,2575],{},[2469,2569,2571],{"href":2570},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Froutes\u002Fwbs.ts",[28,2572,2573],{},"src\u002Froutes\u002Fwbs.ts",":\n",[617,2576,2577,2587],{},[620,2578,2579,2582,2583,2586],{},[28,2580,2581],{},"GET \u002Fwbs"," — 공개. ",[28,2584,2585],{},"loadDoc()","이 R2 객체 없으면 시드를 PUT 후 반환.",[620,2588,2589,170,2592,2595,2596,146,2598,2601,2602,2605,2606,158,2609,243],{},[28,2590,2591],{},"PATCH \u002Fwbs\u002Ftasks\u002F:taskId",[28,2593,2594],{},"requireAuth()"," 미들웨어. body 5필드. ",[28,2597,219],{},[28,2599,2600],{},"delete target[field]"," \u002F ",[28,2603,2604],{},"undefined"," → 유지 \u002F 값 → 갱신. 마지막에 ",[28,2607,2608],{},"lastUpdated = new Date().toISOString().slice(0, 10)",[28,2610,2611],{},"saveDoc()",[19,2613,1972],{},[617,2615,2616,2628],{},[620,2617,2618,170,2624,2627],{},[2469,2619,2621],{"href":2620},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Findex.ts",[28,2622,2623],{},"src\u002Findex.ts",[28,2625,2626],{},"app.route('\u002Fwbs', wbs)"," 등록.",[620,2629,2630,170,2636,2639],{},[2469,2631,2633],{"href":2632},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Fopenapi.ts",[28,2634,2635],{},"src\u002Fopenapi.ts",[28,2637,2638],{},"wbs"," 태그 + 2개 경로(GET 공개·PATCH 401 응답 포함).",[14,2641,2643],{"id":2642},"_54-사용자단-변경-malgn-noti","5.4 사용자단 변경 (malgn-noti)",[19,2645,2646,2650],{},[2469,2647,2648],{"href":2471},[28,2649,2474],{}," 전면 재작성:",[617,2652,2653,2660,2667,2686,2697],{},[620,2654,2655,2656,2659],{},"임베디드 STAGES 제거 → top-level ",[28,2657,2658],{},"await api('\u002Fwbs')","로 비동기 로드. 로딩\u002F에러 상태 노출.",[620,2661,2662,2663,2666],{},"task 행 우측에 ✏️ 편집 버튼 — ",[28,2664,2665],{},"v-if=\"auth.user\"","로 로그인 사용자에게만 노출.",[620,2668,2669,2670,2672,2673],{},"모달 (",[28,2671,1544],{}," 기반): 담당자 \u002F 설명 \u002F 링크 \u002F 목표일 \u002F 완료일 5개 입력.\n",[617,2674,2675,2681],{},[620,2676,2677,2678,2680],{},"빈 문자열 저장 시 payload에 ",[28,2679,219],{}," 전송 → 서버 R2에서 해당 필드 제거.",[620,2682,2683,2685],{},[28,2684,2522],{},"는 빈 값 불가 (Zod min(1)). 빈 값일 땐 payload에서 제외 → 미변경.",[620,2687,2688,2689,2692,2693,2696],{},"저장 성공 시 ",[28,2690,2691],{},"useToast()"," 알림 + ",[28,2694,2695],{},"Object.assign(t, res.data)","로 in-place 갱신(refetch 없음).",[620,2698,2699,2700,2703,2704,2707],{},"비로그인 부제에 ",[28,2701,2702],{},"· 로그인하면 편집 가능"," 힌트 노출 + ",[28,2705,2706],{},"\u002Flogin?redirect=\u002Fwbs"," 링크.",[14,2709,2711],{"id":2710},"_55-배포","5.5 배포",[617,2713,2714,2728],{},[620,2715,2716,2717,2720,2721,2724,2725,2727],{},"API: typecheck → ",[28,2718,2719],{},"pnpm run deploy"," → Version ",[28,2722,2723],{},"28f3e6a8-6b53-42ee-b3d7-a145584f43d0",". 번들 2672 KiB \u002F gzip 609. Worker Startup 75 ms. FILES 바인딩(",[28,2726,2498],{},") 정상.",[620,2729,2730,2731,146,2733,2736,2737,243],{},"Pages: ",[28,2732,1776],{},[28,2734,2735],{},"npx wrangler@4 pages deploy dist --project-name=malgn-noti --branch=main",". alias ",[28,2738,2739],{},"02bb58e6.malgn-noti.pages.dev",[14,2741,2743],{"id":2742},"_56-검증","5.6 검증",[617,2745,2746,2756,2769],{},[620,2747,2748,2751,2752,2755],{},[28,2749,2750],{},"GET \u002Fhealth"," 200 \u002F ",[28,2753,2754],{},"\u002Fhealth\u002Fdb"," 200 (mysql 8.0.42)",[620,2757,2758,2760,2761,2764,2765,2768],{},[28,2759,2581],{}," 200, ",[23,2762,2763],{},"30,406 bytes"," JSON. stages: 5 \u002F tasks: 142 \u002F lastUpdated: ",[28,2766,2767],{},"2026-06-01"," (시드 값 — R2 첫 PUT 직후 그대로). 시드 자동 적재 동작 확인.",[620,2770,2771,2774],{},[28,2772,2773],{},"https:\u002F\u002Fmalgn-noti.pages.dev\u002Fwbs"," 200, alias 200.",[14,2776,2778],{"id":2777},"_57-산출물","5.7 산출물",[617,2780,2781,2787,2793],{},[620,2782,2783,2786],{},[28,2784,2785],{},"malgn-noti-api: 9945db3"," feat(wbs): R2 JSON 정본 + GET 공개 \u002F PATCH 인증 라우트 (4 files, +452)",[620,2788,2789,2792],{},[28,2790,2791],{},"malgn-noti: 3ed473e"," feat(wbs): \u002Fwbs API 연동 + 인라인 편집 모달 (1 file, +376 -355)",[620,2794,2795,2796,2798],{},"R2 객체 ",[28,2797,42],{}," 라이브 (FILES 바인딩, 시드 자동 적재됨)",[14,2800,2802],{"id":2801},"_59-후속-날짜-포맷-정합화-workers-version-eb02206c-pages-alias-98bd09e2","5.9 후속 — 날짜 포맷 정합화 (Workers Version eb02206c \u002F Pages alias 98bd09e2)",[19,2804,2805],{},"사용자 지시: \"목표일과 완료일은 년.월.일 로 변경해 주세요.\"",[617,2807,2808,2834,2846,2856,2865,2882],{},[620,2809,2810,166,2813,2601,2816,2601,2819,2822,2823,2825,2826,2829,2830,2833],{},[23,2811,2812],{},"사용자단",[28,2814,2815],{},"formatYmd",[28,2817,2818],{},"toDateInputValue",[28,2820,2821],{},"fromDateInputValue"," 헬퍼 도입. 어떤 입력(YYYY.MM.DD \u002F YYYY-MM-DD \u002F 레거시 M-D)이든 ",[28,2824,231],{},"로 정규화. 표시에 일괄 적용 — 레거시 ",[28,2827,2828],{},"5\u002F8","은 2026 기준 ",[28,2831,2832],{},"2026.05.08","로 렌더.",[620,2835,2836,2839,2840,2842,2843,2845],{},[23,2837,2838],{},"편집 모달",": 텍스트 입력 → ",[28,2841,235],{}," 두 개로 교체(브라우저 캘린더 위젯). 빈값은 ",[28,2844,219],{},"로 전송 → R2 필드 제거.",[620,2847,2848,2851,2852,2855],{},[23,2849,2850],{},"API"," Zod에 ",[28,2853,2854],{},"^\\d{4}\\.\\d{2}\\.\\d{2}$"," regex 검증 추가, 위반 시 400. OpenAPI 스키마도 동기화.",[620,2857,2858,2861,2862,2864],{},[23,2859,2860],{},"저장 정책",": 신규 PATCH는 무조건 ",[28,2863,231],{},". 기존 R2의 레거시 값은 그대로 두고 다음 편집 시 자연 정합화.",[620,2866,2867,2868,2751,2871,2751,2873,2751,2875,2878,2879,2881],{},"검증: ",[28,2869,2870],{},"\u002Fhealth",[28,2872,2754],{},[28,2874,50],{},[28,2876,2877],{},"PATCH \u002Fwbs\u002Ftasks\u002F1-1-1"," no-auth → 401(정상). prod & alias ",[28,2880,50],{}," 200.",[620,2883,2884,2885,2888,2889,2892,2893,2896,2897,2900],{},"산출물: ",[28,2886,2887],{},"malgn-noti-api: 3a35464"," fix(wbs): targetDate\u002FcompletionDate 포맷 YYYY.MM.DD 강제 (Workers Version ",[28,2890,2891],{},"eb02206c-c076-4f09-8881-5f536feecb02","). ",[28,2894,2895],{},"malgn-noti: 08e5c33"," fix(wbs): 목표일·완료일 YYYY.MM.DD 포맷 + native date picker (Pages alias ",[28,2898,2899],{},"98bd09e2.malgn-noti.pages.dev",").",[14,2902,2904],{"id":2903},"_510-한계-후속","5.10 한계 \u002F 후속",[617,2906,2907,2913,2919],{},[620,2908,2909,2912],{},[23,2910,2911],{},"동시 편집 — last-write-wins",". 두 명이 동시에 다른 task를 PATCH하면 둘 다 R2 read-modify-write를 하므로 한쪽이 사라질 수 있다. 강한 정합 필요 시 ETag(If-Match) 도입.",[620,2914,2915,2918],{},[23,2916,2917],{},"편집 범위 제한",": status \u002F weight \u002F progress \u002F title \u002F group \u002F 단계 추가·삭제는 본 화면에서 안 됨. 필요하면 별도 슬라이스에서 모달 확장 + 권한 강화(owner\u002Fadmin only).",[620,2920,2921,2924],{},[23,2922,2923],{},"lastUpdated"," 자동 갱신만 됨 — 누가 언제 무엇을 바꿨는지 감사 로그는 없음. 운영 단계 진입 전 도입 검토.",[245,2926],{},[245,2928],{},[10,2930,2932],{"id":2931},"_6-nhn-notification-hub-oauth-어댑터-email-실-발송-서비스-담당자-이메일-변경-라우트","§6. NHN Notification Hub OAuth 어댑터 + Email 실 발송 + 서비스 담당자 이메일 변경 라우트",[14,2934,254],{"id":2935},"한-줄-4",[19,2937,2938,2939,2942,2943,2946,2947,948,2950,2953,2954,2957,2958,948,2961,2964,2965,2968,2969,2971,2972,2975,2976,2982,2983,2986,2987,2990],{},"NHN 신규 통합 서비스(Notification Hub)는 기존 AppKey + SecretKey 직접 호출이 아닌 ",[23,2940,2941],{},"OAuth2 client_credentials → Bearer 토큰"," 방식. 어댑터 전면 재작성(",[28,2944,2945],{},"src\u002Fadapters\u002Fnhn\u002Foauth.ts"," 신규 + ",[28,2948,2949],{},"sms.ts",[28,2951,2952],{},"email.ts"," 재작성). Email은 ",[28,2955,2956],{},"message@malgnsoft.com"," 발신 도메인 콘솔 등록 + ",[28,2959,2960],{},"EMAIL_FROM",[28,2962,2963],{},"EMAIL_FROM_NAME"," secret 등록 후 실 발송 검증 통과(messageId 발급). SMS는 콘솔 발신번호 등록 + ",[28,2966,2967],{},"SMS_FROM"," secret 대기. 동시에 ",[28,2970,30],{}," 신설 — OTP(",[28,2973,2974],{},"purpose=change_email",") + 비밀번호 검증 후 ",[23,2977,2978,2981],{},[28,2979,2980],{},"user.email"," 만"," UPDATE(",[28,2984,2985],{},"loginid","는 가입 시 식별자로 고정 유지). 사용자단 ",[28,2988,2989],{},"AppEmailChangeDialog","도 실 API로 교체.",[14,2992,2994],{"id":2993},"_61-어댑터-재작성","6.1 어댑터 재작성",[617,2996,2997,3021,3046,3058],{},[620,2998,2999,170,3001,3004,3005,3008,3009,3012,3013,3016,3017,3020],{},[28,3000,2945],{},[28,3002,3003],{},"https:\u002F\u002Foauth.api.nhncloudservice.com\u002Foauth2\u002Ftoken\u002Fcreate","로 Basic Auth(",[28,3006,3007],{},"userAccessKey:secretAccessKey",") + body ",[28,3010,3011],{},"grant_type=client_credentials&scope=appKey:{APP_KEY}",". 응답 ",[28,3014,3015],{},"access_token","은 메모리 캐시(",[28,3018,3019],{},"globalThis.__nhnTokenCache",", 60s 안전 마진).",[620,3022,3023,170,3026,3029,3030,158,3033,158,3036,3039,3040,158,3043,243],{},[28,3024,3025],{},"src\u002Fadapters\u002Fnhn\u002Fsms.ts",[28,3027,3028],{},"POST {base}\u002Fmessage\u002Fv1.0\u002FSMS\u002Ffree-form-messages\u002F{messagePurpose}",". body: ",[28,3031,3032],{},"sender.senderPhoneNumber",[28,3034,3035],{},"recipients[].contacts[](contactType=PHONE_NUMBER, contact)",[28,3037,3038],{},"content(messageType, body, title?)",". 헤더 ",[28,3041,3042],{},"X-NC-APP-KEY",[28,3044,3045],{},"X-NHN-Authorization: Bearer ...",[620,3047,3048,3051,3052,361,3055,2900],{},[28,3049,3050],{},"src\u002Fadapters\u002Fnhn\u002Femail.ts"," — 동형(",[28,3053,3054],{},"\u002FEMAIL\u002Ffree-form-messages\u002F{purpose}",[28,3056,3057],{},"contactType=EMAIL_ADDRESS",[620,3059,3060,3063,3064,1710,3067,3070,3071,3074,3075,3078],{},[28,3061,3062],{},"NhnCredentials"," 확장 — ",[28,3065,3066],{},"userAccessKey",[28,3068,3069],{},"secretAccessKey"," 추가, 기존 ",[28,3072,3073],{},"secretKey","는 옵셔널로 다운그레이드. push\u002Frcs\u002Fkakao는 ",[28,3076,3077],{},"!creds.secretKey"," 가드로 mock fallback 유지(후속 마이그레이션).",[14,3080,3082],{"id":3081},"_62-email-실-발송-활성화","6.2 Email 실 발송 활성화",[617,3084,3085,3091,3100],{},[620,3086,3087,3088,3090],{},"NHN Notification Hub 콘솔에 ",[28,3089,2956],{}," 발신 도메인 등록 + SPF\u002FDKIM 설정.",[620,3092,3093,3094,2601,3097,2627],{},"Workers secret ",[28,3095,3096],{},"EMAIL_FROM=message@malgnsoft.com",[28,3098,3099],{},"EMAIL_FROM_NAME=맑은 메시징",[620,3101,3102,3103,3106,3107,243],{},"라이브 검증 — ",[28,3104,3105],{},"POST \u002Fsend\u002Femail",": SUCCESS · ",[28,3108,3109],{},"messageId=20260604154608lR6MBAKn8v0",[14,3111,3113,3114,3116],{"id":3112},"_63-post-meemail-change-정책","6.3 ",[28,3115,30],{}," 정책",[617,3118,3119,3128,3135,3144],{},[620,3120,3121,1838,3124,3127],{},[23,3122,3123],{},"유지",[28,3125,3126],{},"user.loginid"," — 가입 시 발급된 식별자이며 전역 UNIQUE. 변경 가능하면 감사·연동 추적·세션이 깨진다.",[620,3129,3130,1838,3132,3134],{},[23,3131,810],{},[28,3133,2980],{}," 한 항목만. 알림·연락처용 이메일이 새 주소로 교체.",[620,3136,3137,3140,3141,3143],{},[23,3138,3139],{},"검증 흐름",": (1) 새 이메일에 OTP(",[28,3142,2974],{},") 발송 → (2) 클라이언트가 본인 비밀번호 + OTP 동시 입력 → (3) 서버에서 비밀번호 PBKDF2 검증 + OTP 코드 검증 + email-only UPDATE + 코드 소비 마킹.",[620,3145,3146],{},"5 시나리오 e2e 통과: 정상 \u002F 코드 만료 \u002F 코드 오타 \u002F 비밀번호 오타 \u002F 동일 이메일 재시도.",[14,3148,3150],{"id":3149},"_64-사용자단-변경","6.4 사용자단 변경",[617,3152,3153,3170,3185,3200],{},[620,3154,3155,1838,3158,948,3161,2238,3164,130,3167,3169],{},[28,3156,3157],{},"AppEmailChangeDialog.vue",[28,3159,3160],{},"sendCode",[28,3162,3163],{},"confirmCode",[28,3165,3166],{},"\u002Fauth\u002Femail-code\u002F{send,verify}",[28,3168,2974],{},")로 교체.",[620,3171,3172,3173,1838,3176,146,3179,368,3182,2900],{},"emit ",[28,3174,3175],{},"confirm",[28,3177,3178],{},"[string]",[28,3180,3181],{},"[EmailChangePayload]",[28,3183,3184],{},"{newEmail, code, password}",[620,3186,3187,1838,3190,3193,3194,146,3196,3199],{},[28,3188,3189],{},"app\u002Fstores\u002Fauth.ts",[28,3191,3192],{},"changeEmail(payload)"," 액션 추가 — ",[28,3195,30],{},[28,3197,3198],{},"user\u002Ftenant"," 갱신.",[620,3201,3202,3205,3206,3209,3210,3212],{},[28,3203,3204],{},"\u002Faccount\u002Fsettings"," 두 다이얼로그 중 서비스 담당자 이메일 변경 다이얼로그가 실 API로 교체. ",[23,3207,3208],{},"결제 이메일 변경","은 ",[28,3211,106],{},"로 별도 흐름 유지.",[14,3214,3216],{"id":3215},"_65-산출물","6.5 산출물",[617,3218,3219,3243,3258],{},[620,3220,880,3221,3223,3224,1710,3226,3228,3229,3232,3233,166,3236,3239,3240,3242],{},[28,3222,2945],{}," 신규 \u002F ",[28,3225,3025],{},[28,3227,2952],{}," 재작성 \u002F ",[28,3230,3231],{},"src\u002Fadapters\u002Fnhn\u002Ftypes.ts"," 확장 \u002F ",[28,3234,3235],{},"src\u002Froutes\u002Fme.ts",[28,3237,3238],{},"POST \u002Femail-change"," 추가 \u002F ",[28,3241,2635],{}," summary 갱신.",[620,3244,3245,3246,3228,3249,166,3251,2601,3254,3257],{},"사용자단: ",[28,3247,3248],{},"app\u002Fcomponents\u002FAppEmailChangeDialog.vue",[28,3250,3189],{},[28,3252,3253],{},"changeEmail",[28,3255,3256],{},"app\u002Fpages\u002Faccount\u002Fsettings.vue"," 토스트 메시지 정정.",[620,3259,3260],{},"배포: Workers Version 갱신(이메일 변경 + NHN OAuth) \u002F Pages alias 갱신.",[14,3262,3264],{"id":3263},"_66-알려진-한계","6.6 알려진 한계",[617,3266,3267,3270,3273],{},[620,3268,3269],{},"push\u002Frcs\u002Fkakao 어댑터는 아직 legacy AppKey + SecretKey 직호출 형태 — Notification Hub 마이그레이션 후속.",[620,3271,3272],{},"SMS 라이브 e2e 1건 미실행 (발신번호 등록 대기).",[620,3274,3275,3276,3279],{},"자격증명은 현재 Worker 환경변수에 직접 보관. envelope 암호화(",[28,3277,3278],{},"NhnCredential"," 테이블 + master key)는 멀티 테넌트 진입 시 도입.",[245,3281],{},[10,3283,3285,3286,3288],{"id":3284},"_7-wbs-현행화-r2-정본-docwbsmd-동시-갱신","§7. WBS 현행화 — R2 정본 + ",[28,3287,46],{}," 동시 갱신",[14,3290,254],{"id":3291},"한-줄-5",[19,3293,3294,3295,158,3297,3299],{},"오늘 §1~§6 작업을 WBS 정본 두 곳(R2 ",[28,3296,42],{},[28,3298,46],{},")에 반영. Step 5 task 9건 신규(5-2-19\u002F20\u002F21, 5-3-15, 5-3C-20, 5-4-14\u002F15\u002F16, 5-5-10\u002F11\u002F12) + 5-3C-7 완료 승급 + 5-2-16·5-5-5 in_progress 승급. 진척률 48% → 55%, 가중평균 약 47.5%.",[14,3301,3303],{"id":3302},"_71-추가갱신된-task","7.1 추가·갱신된 task",[617,3305,3306,3314,3322,3328,3338,3344,3352,3358,3364,3370,3379,3385,3394,3400,3406,3412],{},[620,3307,3308,166,3311,783],{},[23,3309,3310],{},"5-2-19",[28,3312,3313],{},"WBS 정본 R2 저장 + GET\u002FPATCH 라우트",[620,3315,3316,166,3319,3321],{},[23,3317,3318],{},"5-2-20",[28,3320,30],{}," — 서비스 담당자 이메일 변경 ✅",[620,3323,3324,3327],{},[23,3325,3326],{},"5-2-21"," NHN Notification Hub 어댑터 신규(OAuth + Bearer) ✅",[620,3329,3330,3333,3334,3337],{},[23,3331,3332],{},"5-2-14"," PG 어댑터 — ",[23,3335,3336],{},"TossPayments 확정","(메모 갱신)",[620,3339,3340,3343],{},[23,3341,3342],{},"5-2-16"," NHN 실 모드 전환 — ⚪ → 🟢 (OAuth 어댑터 완료, envelope 후속)",[620,3345,3346,166,3349,3351],{},[23,3347,3348],{},"5-3-15",[28,3350,50],{}," 페이지 — R2 정본 비동기 로드 + 인라인 편집 모달 ✅",[620,3353,3354,3357],{},[23,3355,3356],{},"5-3C-7"," 회원 정보 변경 — 🟢 → ✅ (이메일 변경 OTP 연결 완료)",[620,3359,3360,3363],{},[23,3361,3362],{},"5-3C-20"," 서비스 담당자 이메일 변경 — 실 OTP API 연동 ✅",[620,3365,3366,3369],{},[23,3367,3368],{},"5-4-14"," 관리자단 핸드오프 정본 17 페이지 풀세트 ✅",[620,3371,3372,3375,3376,783],{},[23,3373,3374],{},"5-4-15"," 페이지 진척 상태 라벨 ",[28,3377,3378],{},"dev=screen\u002Fpartial\u002Flive",[620,3380,3381,3384],{},[23,3382,3383],{},"5-4-16"," 관리자 로고\u002F브랜드 — 사용자단 로고로 통일 + \"관리자\" 배지 ✅",[620,3386,3387,948,3390,3393],{},[23,3388,3389],{},"5-5-1",[28,3391,3392],{},"5-5-3"," 배포 카운터 갱신",[620,3395,3396,3399],{},[23,3397,3398],{},"5-5-5"," NHN Notification Hub real — ⚪ → 🟢 (Email ✅, SMS pending)",[620,3401,3402,3405],{},[23,3403,3404],{},"5-5-10"," Hyperdrive Cloudflare Tunnel(Access) 전환 ✅",[620,3407,3408,3411],{},[23,3409,3410],{},"5-5-11"," NHN Email 실 발송 활성화 ✅",[620,3413,3414,3417],{},[23,3415,3416],{},"5-5-12"," NHN SMS 실 발송 활성화 ⚪ (발신번호 등록 대기)",[14,3419,3421],{"id":3420},"_72-산출물","7.2 산출물",[617,3423,3424,3431,3437],{},[620,3425,3426,3427,3430],{},"R2: ",[28,3428,3429],{},"malgn-noti-files\u002Fwbs\u002Fwbs.json"," 업로드 (142 → 155 task).",[620,3432,3433,3434,3436],{},"정본 MD: ",[28,3435,46],{}," 스냅샷 표 \u002F 5-2 \u002F 5-3A \u002F 5-3M \u002F 5-3C \u002F 5-4 \u002F 5-5 \u002F \"알려진 한계\" 갱신.",[620,3438,3439,3440,3443],{},"라이브 검증: ",[28,3441,3442],{},"GET https:\u002F\u002Fmalgn-noti-api.malgnsoft.workers.dev\u002Fwbs"," → 155 task 확인.",[14,3445,3447],{"id":3446},"_73-한계-후속","7.3 한계 \u002F 후속",[617,3449,3450,3453],{},[620,3451,3452],{},"Step 1·2·3·4의 진척률은 6\u002F4 작업과 무관해 그대로(55\u002F55\u002F35\u002F20%). 컨설팅팀·기획팀 작업이 들어오면 별도 갱신.",[620,3454,3455],{},"\"WBS 현행화\" 자체의 감사 로그(누가 언제 무엇을 PATCH 했는지)는 미 — 운영 진입 시 도입.",[245,3457],{},[245,3459],{},[10,3461,3463],{"id":3462},"_8-서비스-담당자-이메일-변경-401-자동-로그아웃-otp-이중-소비-다이얼로그-데드락-fix-workers-version-772adb3b-pages-alias-cd6a5bb3","§8. 서비스 담당자 이메일 변경 — 401 자동 로그아웃 + OTP 이중 소비 + 다이얼로그 데드락 fix (Workers Version 772adb3b \u002F Pages alias cd6a5bb3)",[14,3465,254],{"id":3466},"한-줄-6",[19,3468,3469,3470,3472,3473,3475,3476,3478,3479,3482,3483,59,3485,3488,3489,3492,3493,87,3495,3497],{},"사용자 보고 \"변경 완료 후 로그아웃 + 이메일 미변경\". 추적 결과 세 단계가 겹쳐 발생: (a) 다이얼로그 \"확인\" → ",[28,3471,58],{}," 가 verification row 의 ",[28,3474,66],{}," 을 마킹 → (b) 직후 \"변경\" → ",[28,3477,70],{}," 가 다시 OTP 검증 시 ",[28,3480,3481],{},"isNull(consumedAt)"," 매치 실패 → 401 → (c) ",[28,3484,74],{},[28,3486,3487],{},"\u002Fauth\u002F*"," 외 401 을 토큰 만료로 간주해 자동 로그아웃 + ",[28,3490,3491],{},"\u002Flogin"," 이동. 추가로 다이얼로그는 ",[28,3494,86],{},[28,3496,90],{}," 를 기다려서, 에러 토스트는 떴지만 \"변경 중…\" 버튼이 영원히 잠겨 다시 시도 불가.",[14,3499,3501],{"id":3500},"_81-a-otp-이중-소비-verify-only-분기","8.1 (a) OTP 이중 소비 — verify-only 분기",[617,3503,3504,3516],{},[620,3505,3506,3507,3512,3513,3515],{},"다른 purpose (signup·reset_password·contract_sign) 는 verify 한 번에 검증·소비가 끝나는 흐름이라 그대로 두고, ",[23,3508,3509,2981],{},[28,3510,3511],{},"change_email"," verify 단계에서 ",[28,3514,66],{}," 마킹 건너뜀.",[620,3517,3518,3519,3521],{},"최종 ",[28,3520,70],{}," 가 한 번 더 검증 + UPDATE + 소비.",[19,3523,1972],{},[332,3525,3527],{"className":334,"code":3526,"language":336,"meta":337,"style":337},"\u002F\u002F src\u002Froutes\u002Fauth.ts — POST \u002Fauth\u002Femail-code\u002Fverify\nif (purpose !== 'change_email') {\n  await db.update(verification)\n    .set({ consumedAt: now })\n    .where(eq(verification.id, row.id))\n}\n",[28,3528,3529,3534,3550,3564,3575,3590],{"__ignoreMap":337},[341,3530,3531],{"class":343,"line":344},[341,3532,3533],{"class":2022},"\u002F\u002F src\u002Froutes\u002Fauth.ts — POST \u002Fauth\u002Femail-code\u002Fverify\n",[341,3535,3536,3539,3542,3545,3548],{"class":343,"line":384},[341,3537,3538],{"class":364},"if",[341,3540,3541],{"class":347}," (purpose ",[341,3543,3544],{"class":364},"!==",[341,3546,3547],{"class":357}," 'change_email'",[341,3549,443],{"class":347},[341,3551,3552,3555,3558,3561],{"class":343,"line":409},[341,3553,3554],{"class":364},"  await",[341,3556,3557],{"class":347}," db.",[341,3559,3560],{"class":351},"update",[341,3562,3563],{"class":347},"(verification)\n",[341,3565,3566,3569,3572],{"class":343,"line":446},[341,3567,3568],{"class":347},"    .",[341,3570,3571],{"class":351},"set",[341,3573,3574],{"class":347},"({ consumedAt: now })\n",[341,3576,3577,3579,3582,3584,3587],{"class":343,"line":478},[341,3578,3568],{"class":347},[341,3580,3581],{"class":351},"where",[341,3583,130],{"class":347},[341,3585,3586],{"class":351},"eq",[341,3588,3589],{"class":347},"(verification.id, row.id))\n",[341,3591,3592],{"class":343,"line":499},[341,3593,1933],{"class":347},[14,3595,3597],{"id":3596},"_82-b-401-422-분리","8.2 (b) 401 → 422 분리",[19,3599,3600,3603,3604,3607],{},[28,3601,3602],{},"errors.unauthenticated()"," 401 은 ",[23,3605,3606],{},"토큰 검증 실패(미들웨어 단계)"," 에만 한정하기로 의미 정리. 본인 재인증(비밀번호·OTP) 실패는 새 헬퍼:",[332,3609,3611],{"className":334,"code":3610,"language":336,"meta":337,"style":337},"\u002F\u002F src\u002Flib\u002Ferrors.ts\nunprocessable: (msg: string) => new AppError('unprocessable', 422, msg),\n",[28,3612,3613,3618],{"__ignoreMap":337},[341,3614,3615],{"class":343,"line":344},[341,3616,3617],{"class":2022},"\u002F\u002F src\u002Flib\u002Ferrors.ts\n",[341,3619,3620,3623,3626,3629,3631,3633,3635,3637,3640,3643,3645,3648,3650,3653],{"class":343,"line":384},[341,3621,3622],{"class":351},"unprocessable",[341,3624,3625],{"class":347},": (",[341,3627,3628],{"class":371},"msg",[341,3630,394],{"class":364},[341,3632,397],{"class":390},[341,3634,375],{"class":347},[341,3636,378],{"class":364},[341,3638,3639],{"class":364}," new",[341,3641,3642],{"class":351}," AppError",[341,3644,130],{"class":347},[341,3646,3647],{"class":357},"'unprocessable'",[341,3649,361],{"class":347},[341,3651,3652],{"class":390},"422",[341,3654,3655],{"class":347},", msg),\n",[19,3657,3658,3660,3661,3663],{},[28,3659,70],{}," 의 비밀번호 불일치·OTP 만료·OTP 불일치 throw 를 모두 ",[28,3662,78],{}," 로 교체. 클라이언트의 자동 로그아웃 트리거 회피 + 의미 정확.",[14,3665,3667],{"id":3666},"_83-c-다이얼로그-submit-데드락-async-callback-prop","8.3 (c) 다이얼로그 submit 데드락 — async callback prop",[19,3669,3670,3671,146,3673,158,3675,3678,3679,3682,3683,3686,3687,83,3690,3693,3694,3697],{},"기존: ",[28,3672,82],{},[28,3674,86],{},[28,3676,3677],{},"emit('confirm', payload)"," → 부모가 ",[28,3680,3681],{},"await"," 성공 시 ",[28,3684,3685],{},"emit('close')"," → 다이얼로그 ",[28,3688,3689],{},"watch(open)",[28,3691,3692],{},"reset()"," 로 ",[28,3695,3696],{},"submitting=false",". 에러 시 부모가 toast 만 띄우고 close 하지 않으므로 데드락.",[19,3699,3700,3701,3703,3704,158,3706,3709],{},"변경: 다이얼로그 prop 에 ",[28,3702,94],{}," 추가. 다이얼로그가 직접 ",[28,3705,3681],{},[28,3707,3708],{},"try\u002Fcatch\u002Ffinally"," 로 자기 상태 관리.",[332,3711,3713],{"className":334,"code":3712,"language":336,"meta":337,"style":337},"\u002F\u002F AppEmailChangeDialog.vue\nasync function submit() {\n  if (submitting.value) return\n  submitting.value = true\n  try {\n    await props.onConfirm({ newEmail: ..., code: ..., password: ... })\n    emit('close')\n  } catch (e) {\n    toast.add({ title: msgFromError(e), color: 'error' })\n  } finally {\n    submitting.value = false   \u002F\u002F 성공·실패 모두 잠금 해제\n  }\n}\n",[28,3714,3715,3720,3733,3744,3754,3761,3791,3804,3815,3837,3847,3861,3866],{"__ignoreMap":337},[341,3716,3717],{"class":343,"line":344},[341,3718,3719],{"class":2022},"\u002F\u002F AppEmailChangeDialog.vue\n",[341,3721,3722,3724,3727,3730],{"class":343,"line":384},[341,3723,365],{"class":364},[341,3725,3726],{"class":364}," function",[341,3728,3729],{"class":351}," submit",[341,3731,3732],{"class":347},"() {\n",[341,3734,3735,3738,3741],{"class":343,"line":409},[341,3736,3737],{"class":364},"  if",[341,3739,3740],{"class":347}," (submitting.value) ",[341,3742,3743],{"class":364},"return\n",[341,3745,3746,3749,3751],{"class":343,"line":446},[341,3747,3748],{"class":347},"  submitting.value ",[341,3750,403],{"class":364},[341,3752,3753],{"class":390}," true\n",[341,3755,3756,3759],{"class":343,"line":478},[341,3757,3758],{"class":364},"  try",[341,3760,381],{"class":347},[341,3762,3763,3766,3769,3772,3775,3778,3781,3783,3786,3788],{"class":343,"line":499},[341,3764,3765],{"class":364},"    await",[341,3767,3768],{"class":347}," props.",[341,3770,3771],{"class":351},"onConfirm",[341,3773,3774],{"class":347},"({ newEmail: ",[341,3776,3777],{"class":364},"...",[341,3779,3780],{"class":347},", code: ",[341,3782,3777],{"class":364},[341,3784,3785],{"class":347},", password: ",[341,3787,3777],{"class":364},[341,3789,3790],{"class":347}," })\n",[341,3792,3793,3796,3798,3801],{"class":343,"line":511},[341,3794,3795],{"class":351},"    emit",[341,3797,130],{"class":347},[341,3799,3800],{"class":357},"'close'",[341,3802,3803],{"class":347},")\n",[341,3805,3806,3809,3812],{"class":343,"line":517},[341,3807,3808],{"class":347},"  } ",[341,3810,3811],{"class":364},"catch",[341,3813,3814],{"class":347}," (e) {\n",[341,3816,3817,3820,3823,3826,3829,3832,3835],{"class":343,"line":540},[341,3818,3819],{"class":347},"    toast.",[341,3821,3822],{"class":351},"add",[341,3824,3825],{"class":347},"({ title: ",[341,3827,3828],{"class":351},"msgFromError",[341,3830,3831],{"class":347},"(e), color: ",[341,3833,3834],{"class":357},"'error'",[341,3836,3790],{"class":347},[341,3838,3840,3842,3845],{"class":343,"line":3839},10,[341,3841,3808],{"class":347},[341,3843,3844],{"class":364},"finally",[341,3846,381],{"class":347},[341,3848,3850,3853,3855,3858],{"class":343,"line":3849},11,[341,3851,3852],{"class":347},"    submitting.value ",[341,3854,403],{"class":364},[341,3856,3857],{"class":390}," false",[341,3859,3860],{"class":2022},"   \u002F\u002F 성공·실패 모두 잠금 해제\n",[341,3862,3864],{"class":343,"line":3863},12,[341,3865,514],{"class":347},[341,3867,3869],{"class":343,"line":3868},13,[341,3870,1933],{"class":347},[19,3872,3873,3874,3877,3878,3881],{},"부모(",[28,3875,3876],{},"AppMemberInfoPanel.vue",") 는 ",[28,3879,3880],{},":on-confirm=\"handleEmailConfirm\""," 로 비동기 함수만 전달. 성공 toast 만 부모가 띄우고, 에러 toast 는 다이얼로그가 처리.",[14,3883,3885],{"id":3884},"_84-검증","8.4 검증",[617,3887,3888,3896],{},[620,3889,3890,3891,2751,3893,3895],{},"라이브 ",[28,3892,2870],{},[28,3894,70],{}," 401(인증 미들웨어, 토큰 없는 호출) 정상.",[620,3897,3898],{},"다이얼로그: 비밀번호 오타 → 422 → 에러 토스트 + 버튼 활성화 + 재시도 가능. OTP 만료 → 422 → 동일 흐름. 정상 케이스 → 200 → 성공 토스트 + 다이얼로그 닫힘 + 새 이메일 반영.",[14,3900,3902],{"id":3901},"_85-산출물","8.5 산출물",[617,3904,3905,3926,3942],{},[620,3906,3907,1838,3909,3912,3913,3916,3917,368,3919,146,3922,3925],{},[28,3908,883],{},[28,3910,3911],{},"src\u002Flib\u002Ferrors.ts"," (unprocessable 추가) \u002F ",[28,3914,3915],{},"src\u002Froutes\u002Fauth.ts"," (verify-only 분기) \u002F ",[28,3918,3235],{},[28,3920,3921],{},"errors.unauthenticated",[28,3923,3924],{},"errors.unprocessable"," 3 곳).",[620,3927,3928,1838,3931,3933,3934,3937,3938,3941],{},[28,3929,3930],{},"malgn-noti",[28,3932,3248],{}," (prop onConfirm 도입, emit confirm 제거) \u002F ",[28,3935,3936],{},"app\u002Fcomponents\u002FAppMemberInfoPanel.vue"," (handler 시그니처 변경 + ",[28,3939,3940],{},":on-confirm"," 바인딩).",[620,3943,3944,3945,3948,3949,243],{},"배포: Workers Version ",[28,3946,3947],{},"772adb3b-cb30-4e43-b539-7d60254c6195"," \u002F Pages alias ",[28,3950,3951],{},"cd6a5bb3.malgn-noti.pages.dev",[14,3953,3955],{"id":3954},"_86-알려진-한계-후속","8.6 알려진 한계 \u002F 후속",[617,3957,3958,3968],{},[620,3959,3960,3961,361,3964,3967],{},"비슷한 본인 재인증 라우트(예정: ",[28,3962,3963],{},"\u002Fme\u002Fpassword",[28,3965,3966],{},"\u002Fme\u002Fsecurity"," 2FA)도 같은 패턴 적용 — 비밀번호·OTP 오류는 401 이 아닌 422 로 응답.",[620,3969,3970,3972],{},[28,3971,58],{}," 가 purpose 별로 consume 정책이 다른 분기를 갖게 됐다. 다른 흐름(예: 휴대폰 변경)이 추가되면 동일 패턴 확장 검토.",[245,3974],{},[10,3976,3978],{"id":3977},"_9-광고성-메일-수신-동의거부-일시-기록-ddl-0006-workers-version-3671ce95-pages-alias-0f43b158","§9. 광고성 메일 수신 동의\u002F거부 일시 기록 (DDL 0006 \u002F Workers Version 3671ce95 \u002F Pages alias 0f43b158)",[14,3980,254],{"id":3981},"한-줄-7",[19,3983,3984,3985,3987],{},"정보통신망법 50조에 따라 광고성 정보 수신 동의는 동의·거부 일시를 보관해야 한다. ",[28,3986,102],{}," 신규 컬럼 + 사용자 의사 표시 시점마다 갱신 + 사용자단 패널에 마지막 변경 시각 노출 + 토스트에 처리 일시 표기.",[14,3989,3991],{"id":3990},"_91-ddl-0006_company_ad_receive_atsql","9.1 DDL (0006_company_ad_receive_at.sql)",[332,3993,3997],{"className":3994,"code":3995,"language":3996,"meta":337,"style":337},"language-sql shiki shiki-themes github-light github-dark","ALTER TABLE TB_COMPANY\n  ADD COLUMN ad_receive_at DATETIME NULL AFTER ad_receive;\n","sql",[28,3998,3999,4004],{"__ignoreMap":337},[341,4000,4001],{"class":343,"line":344},[341,4002,4003],{},"ALTER TABLE TB_COMPANY\n",[341,4005,4006],{"class":343,"line":384},[341,4007,4008],{},"  ADD COLUMN ad_receive_at DATETIME NULL AFTER ad_receive;\n",[617,4010,4011,4017],{},[620,4012,4013,4014,243],{},"Aurora 직결로 적용 + 컬럼 확인: ",[28,4015,4016],{},"ad_receive_at | datetime | YES | | NULL",[620,4018,4019],{},"기존 행은 모두 NULL — 의사 표시 이력이 없다는 의미. 다음 변경 시점부터 기록.",[14,4021,4023],{"id":4022},"_92-api-변경","9.2 API 변경",[617,4025,4026,4035,4047],{},[620,4027,4028,170,4031,4034],{},[28,4029,4030],{},"src\u002Fdb\u002Fschema.ts",[28,4032,4033],{},"adReceiveAt: datetime('ad_receive_at')"," nullable.",[620,4036,4037,166,4039,170,4041,4043,4044,4046],{},[28,4038,3235],{},[28,4040,106],{},[28,4042,109],{}," 가 있으면 ",[28,4045,113],{}," 동시 set. 같은 값으로 다시 눌러도 의사 표시 갱신으로 간주해 시각 갱신.",[620,4048,4049,4052,4053,285,4056,285,4059,285,4061,4063,4064,4067],{},[28,4050,4051],{},"readContext"," + 4 개 응답 라우트(",[28,4054,4055],{},"GET \u002Fme",[28,4057,4058],{},"PATCH \u002Fme",[28,4060,106],{},[28,4062,30],{},") 모두 ",[28,4065,4066],{},"company.adReceiveAt"," 포함.",[14,4069,4071],{"id":4070},"_93-사용자단-변경","9.3 사용자단 변경",[617,4073,4074,4082],{},[620,4075,4076,170,4078,4081],{},[28,4077,3189],{},[28,4079,4080],{},"AuthCompany.adReceiveAt?: string | null"," 타입 추가.",[620,4083,4084,2574,4086],{},[28,4085,3936],{},[617,4087,4088,4098,4106,4116],{},[620,4089,4090,4093,4094,4097],{},[28,4091,4092],{},"adReceiveAtLabel"," computed — ",[28,4095,4096],{},"YYYY.MM.DD HH:mm"," 포맷 (JetBrains Mono \u002F tabular-nums).",[620,4099,4100,4093,4103,243],{},[28,4101,4102],{},"adReceiveNotice",[28,4104,4105],{},"\"YYYY.MM.DD HH:mm 광고성 메일 수신에 동의\u002F거부함\"",[620,4107,4108,4109,158,4112,4115],{},"토글 옆에 회색 칩(",[28,4110,4111],{},".ad-stamp",[28,4113,4114],{},"i-lucide-clock-3"," 아이콘)으로 노출. 한 번도 변경한 적 없는 회사(기존 데이터)는 칩 미표시.",[620,4117,4118,4119,4122],{},"토스트 성공 메시지 ",[28,4120,4121],{},"description: 처리 일시: YYYY.MM.DD HH:mm"," 추가.",[14,4124,4126],{"id":4125},"_94-검증","9.4 검증",[617,4128,4129,4138],{},[620,4130,3890,4131,4134,4135,4137],{},[28,4132,4133],{},"\u002Fme"," 응답에 ",[28,4136,4066],{}," 포함 확인.",[620,4139,4140,4141,4144],{},"패널에서 수신동의\u002F거부 토글 → 토스트에 일시 + 칩에 새 일시 즉시 반영 (",[28,4142,4143],{},"updateCompany"," 가 응답으로 store hydrate).",[14,4146,4148],{"id":4147},"_95-산출물","9.5 산출물",[617,4150,4151,4163,4171],{},[620,4152,4153,1838,4155,2601,4158,2601,4160,4162],{},[28,4154,883],{},[28,4156,4157],{},"src\u002Fdb\u002Fmigrations\u002F0006_company_ad_receive_at.sql",[28,4159,4030],{},[28,4161,3235],{}," (응답 4 곳 + UPDATE 1 곳).",[620,4164,4165,1838,4167,2601,4169,243],{},[28,4166,3930],{},[28,4168,3189],{},[28,4170,3936],{},[620,4172,3944,4173,3948,4176,243],{},[28,4174,4175],{},"3671ce95-be05-4ba0-8611-60bd168e7b80",[28,4177,4178],{},"0f43b158.malgn-noti.pages.dev",[14,4180,4182],{"id":4181},"_96-알려진-한계-후속","9.6 알려진 한계 \u002F 후속",[617,4184,4185,4196,4199],{},[620,4186,4187,4188,4191,4192,4195],{},"의사 표시 ",[23,4189,4190],{},"마지막 시점","만 보관 — 이력(언제 동의→거부→동의→…)은 미. 광고성 메시지 발송 분쟁 시 시점 증빙용으로 마지막 값만으로도 충분하지만, 운영 진입 시 별도 ",[28,4193,4194],{},"TB_AD_RECEIVE_LOG"," 도입 검토.",[620,4197,4198],{},"IP·User-Agent 등 의사 표시 환경 정보는 미기록. 분쟁 대응 강화 시 추가.",[620,4200,4201,4202,4205],{},"광고 수신 거부자에 대한 발송 차단 로직(쿼리 조건)은 이번 변경 범위 밖. ",[28,4203,4204],{},"dispatch"," producer 측에서 별도 처리.",[245,4207],{},[14,4209,4211],{"id":4210},"한계-다음-단계-오늘-누적","한계 \u002F 다음 단계 (오늘 누적)",[617,4213,4214,4220,4229,4245,4254,4259],{},[620,4215,4216,4219],{},[23,4217,4218],{},"NICE real 전환"," (§1·6\u002F2 §16) — 사용자가 IP 정책 결정 대기. IPv6 대역 등록 또는 검사 OFF.",[620,4221,4222,4225,4226,4228],{},[23,4223,4224],{},"NHN Notification Hub real 전환"," (6\u002F2 §16 + 6\u002F4 §6) — Email ✅ 라이브 검증 통과. SMS는 NHN 콘솔 발신번호 등록 + ",[28,4227,2967],{}," secret 대기. push\u002Frcs\u002Fkakao 어댑터는 Notification Hub로 마이그레이션 미.",[620,4230,4231,4234,4235,158,4238,948,4241,4244],{},[23,4232,4233],{},"PG = TossPayments 확정"," (6\u002F4) — ",[28,4236,4237],{},"src\u002Fadapters\u002Fpg\u002Ftoss.ts",[28,4239,4240],{},"TOSS_CLIENT_KEY",[28,4242,4243],{},"TOSS_SECRET_KEY"," + 카드 등록\u002F결제\u002F취소\u002Fwebhook 미구현.",[620,4246,4247,4250,4251,4253],{},[23,4248,4249],{},"운영자단 P0 진입"," — admin 셸·페이지 완성됐으므로 다음 단계는 (a) 운영자 인증·RBAC, (b) ",[28,4252,1705],{}," 백엔드 라우트 신설(특히 사업자 승인 화면 연동), (c) 실 API 연동.",[620,4255,4256,4258],{},[23,4257,912],{}," (§2.5) — Tunnel 전환 후속 정리.",[620,4260,4261,4264],{},[23,4262,4263],{},"deploy 사고 재발 방지"," (§4.4) — 멀티 레포 deploy 가드 도입 검토.",[4266,4267,4268],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":337,"searchDepth":409,"depth":409,"links":4270},[4271,4272,4273,4274,4275,4276,4277,4278,4279,4280,4281,4282,4283,4284,4285,4286,4287,4288,4289,4290,4291,4292,4293,4294,4295,4296,4297,4298,4299,4300,4301,4302,4303,4304,4305,4306,4307,4308,4309,4310,4312,4313,4314,4315,4316,4317,4318,4319,4320,4321,4322,4323,4324,4325,4326,4327,4328,4329,4330,4331,4332,4333],{"id":16,"depth":384,"text":17},{"id":253,"depth":384,"text":254},{"id":310,"depth":384,"text":311},{"id":329,"depth":384,"text":330},{"id":555,"depth":384,"text":556},{"id":614,"depth":384,"text":615},{"id":645,"depth":384,"text":646},{"id":665,"depth":384,"text":254},{"id":696,"depth":384,"text":697},{"id":772,"depth":384,"text":773},{"id":797,"depth":384,"text":798},{"id":874,"depth":384,"text":875},{"id":904,"depth":384,"text":905},{"id":938,"depth":384,"text":254},{"id":959,"depth":384,"text":960},{"id":1312,"depth":384,"text":1313},{"id":1378,"depth":384,"text":1379},{"id":1584,"depth":384,"text":1585},{"id":1627,"depth":384,"text":1628},{"id":1659,"depth":384,"text":1660},{"id":1674,"depth":384,"text":1675},{"id":1690,"depth":384,"text":1691},{"id":1747,"depth":384,"text":254},{"id":1803,"depth":384,"text":1804},{"id":2067,"depth":384,"text":2068},{"id":2353,"depth":384,"text":2354},{"id":2376,"depth":384,"text":2377},{"id":2461,"depth":384,"text":2462},{"id":2486,"depth":384,"text":2487},{"id":2546,"depth":384,"text":2547},{"id":2642,"depth":384,"text":2643},{"id":2710,"depth":384,"text":2711},{"id":2742,"depth":384,"text":2743},{"id":2777,"depth":384,"text":2778},{"id":2801,"depth":384,"text":2802},{"id":2903,"depth":384,"text":2904},{"id":2935,"depth":384,"text":254},{"id":2993,"depth":384,"text":2994},{"id":3081,"depth":384,"text":3082},{"id":3112,"depth":384,"text":4311},"6.3 POST \u002Fme\u002Femail-change 정책",{"id":3149,"depth":384,"text":3150},{"id":3215,"depth":384,"text":3216},{"id":3263,"depth":384,"text":3264},{"id":3291,"depth":384,"text":254},{"id":3302,"depth":384,"text":3303},{"id":3420,"depth":384,"text":3421},{"id":3446,"depth":384,"text":3447},{"id":3466,"depth":384,"text":254},{"id":3500,"depth":384,"text":3501},{"id":3596,"depth":384,"text":3597},{"id":3666,"depth":384,"text":3667},{"id":3884,"depth":384,"text":3885},{"id":3901,"depth":384,"text":3902},{"id":3954,"depth":384,"text":3955},{"id":3981,"depth":384,"text":254},{"id":3990,"depth":384,"text":3991},{"id":4022,"depth":384,"text":4023},{"id":4070,"depth":384,"text":4071},{"id":4125,"depth":384,"text":4126},{"id":4147,"depth":384,"text":4148},{"id":4181,"depth":384,"text":4182},{"id":4210,"depth":384,"text":4211},"md",{},true,"\u002Fhistory\u002Fhistory.20260604",{"title":5,"description":337},"history\u002Fhistory.20260604","11pZBKhXlZAMqIh7Ou9dsNSnRiaas8pHcLl-tind5m0",1780639567004]