[{"data":1,"prerenderedAt":3420},["ShallowReactive",2],{"doc:\u002Fhistory\u002Fhistory.20260601":3},{"id":4,"title":5,"body":6,"description":352,"extension":3413,"meta":3414,"navigation":3415,"path":3416,"seo":3417,"stem":3418,"__hash__":3419},"docs\u002Fhistory\u002Fhistory.20260601.md","2026-06-01 — WBS 정본화 — doc\u002FWBS.md 신규 + 사용자단 \u002Fwbs 라이브 카탈로그 (배포 #47)",{"type":7,"value":8,"toc":3359},"minimark",[9,23,28,64,68,140,144,227,231,239,245,312,319,325,638,642,694,698,754,757,788,792,855,858,865,869,905,909,984,1010,1014,1079,1083,1126,1130,1211,1215,1298,1305,1309,1373,1380,1425,1429,1453,1457,1518,1520,1528,1531,1558,1562,1568,1680,1690,1694,1713,1843,1851,1855,1893,1897,1941,1945,1956,1960,1976,1978,1990,1993,2048,2052,2061,2128,2143,2147,2197,2201,2417,2421,2432,2440,2452,2461,2465,2468,2555,2564,2568,2584,2588,2612,2616,2657,2661,2744,2746,2757,2760,2798,2802,2899,2905,3023,3029,3092,3096,3102,3213,3230,3233,3237,3268,3272,3292,3296,3353,3355],[10,11,13,14,18,19,22],"h1",{"id":12},"_2026-06-01-wbs-정본화-docwbsmd-신규-사용자단-wbs-라이브-카탈로그-배포-47","2026-06-01 — WBS 정본화 — ",[15,16,17],"code",{},"doc\u002FWBS.md"," 신규 + 사용자단 ",[15,20,21],{},"\u002Fwbs"," 라이브 카탈로그 (배포 #47)",[24,25,27],"h2",{"id":26},"한-줄-요약","한 줄 요약",[29,30,31,34,35,38,39,43,44,46,47,59,60,63],"p",{},[15,32,33],{},"malgn-helper\u002Fdoc\u002FWBS.md"," 양식 + ",[15,36,37],{},"malgn-helper-pms\u002Fpages\u002Fwbs.vue"," Notion soft SaaS 디자인을 차용해 ",[40,41,42],"strong",{},"맑은 메시징 프로젝트 WBS 정본","을 두 산출물로 정착. ",[15,45,17],{},"(텍스트 정본 — 진행률 스냅샷·5단계 가중치·Step 1",[48,49,50,51,54,55,58],"del",{},"5 작업 내역·알려진 한계)와 ",[15,52,53],{},"app\u002Fpages\u002Fwbs.vue","(공개 라이브 카탈로그 — Hero stats·단계별 진행률 오버뷰·Stage 상세·그룹별 작업 카드·상태 칩·외부 링크). Step 5(서비스 개발)는 원본 WBS의 채널·도메인 단위 항목(대부분 0%)을 ",[40,56,57],{},"2026-06-01까지의 실제 진행","(사용자단 6채널 + 전 도메인 화면 완료 \u002F API 5채널·인증·OpenAPI·Queues·webhook 일부 완료 \u002F 관리자단 셸+기획 \u002F 배포 #1","#8)에 맞춰 5-1 설계·5-2 API·5-3 사용자단·5-4 관리자단·5-5 통합·배포의 5 그룹 58 작업으로 재정렬. Cloudflare Pages 프로덕션 배포 #47 (alias ",[15,61,62],{},"0ecc825e.malgn-noti.pages.dev",").",[24,65,67],{"id":66},"_1-사전-조사","1. 사전 조사",[69,70,71,88,113,127],"ul",{},[72,73,74,77,78,87],"li",{},[40,75,76],{},"MD 양식 정본",": ",[79,80,84],"a",{"href":81,"rel":82},"https:\u002F\u002Fgithub.com\u002Fmalgnsoft\u002Fmalgn-helper\u002Fblob\u002Fmain\u002Fdoc\u002FWBS.md",[83],"nofollow",[15,85,86],{},"malgnsoft\u002Fmalgn-helper\u002Fblob\u002Fmain\u002Fdoc\u002FWBS.md"," — Phase 별 진행률 스냅샷 + 단계별 가중치 표 + 단계 상세 표(ID\u002F작업\u002F상태\u002F산출물\u002F비고) + 상태 범례(✅\u002F🟢\u002F⚪\u002F⛔).",[72,89,90,77,93,100,101,104,105,108,109,112],{},[40,91,92],{},"디자인 정본",[79,94,97],{"href":95,"rel":96},"https:\u002F\u002Fgithub.com\u002Fmalgnsoft\u002Fmalgn-helper-pms",[83],[15,98,99],{},"malgnsoft\u002Fmalgn-helper-pms"," ",[15,102,103],{},"pages\u002Fwbs.vue"," — Notion·Linear·Height 풍 \"Soft SaaS\". ",[15,106,107],{},"UContainer max-w-5xl"," + ",[15,110,111],{},"rounded-xl border border-neutral-200"," 카드 + 단계 이모지(🎯📐🛠️📚🧪🚀) + 가중평균 hero + 단계별 오버뷰 행 + Stage 상세 + 상태 칩(emerald\u002Famber\u002Fneutral\u002Frose) + 진척률 막대 컬러 룰(≥70 emerald \u002F ≥30 amber \u002F >0 neutral-400 \u002F 0 neutral-200).",[72,114,115,118,119,122,123,126],{},[40,116,117],{},"데이터 소스",": 사용자가 제공한 7장 스크린샷(Google Sheets 형식의 원본 WBS — ",[15,120,121],{},"맑은메시지(가칭) 프로젝트 작업 내역","). Step 1",[48,124,125],{},"5 전체와 담당자\u002F목표일\u002F완료일\u002F진척율(0","100%) 포함.",[72,128,129,77,132,135,136,139],{},[40,130,131],{},"Step 5 재구성 근거",[15,133,134],{},"doc\u002Fhistory\u002Fhistory.20260511~20260527.md"," 12 일치 누적 이력 — 사용자단 화면 15 종 ✅ \u002F API 9 종 ✅ + 3 종 🟢 + 4 종 ⚪ \u002F 관리자단 ✅ 셸·기획·디자인 가이드 + ⚪ 페이지 11 종 \u002F 배포 사용자단 #1~#46·관리자단 #1·API #1~#8 + DDL ",[15,137,138],{},"0002_export_flow.sql"," ⛔(Cloudflare 1105).",[24,141,143],{"id":142},"_2-결정-사항","2. 결정 사항",[69,145,146,156,168,174,180,206],{},[72,147,148,151,152,155],{},[40,149,150],{},"레포 위치",": 사용자단 ",[15,153,154],{},"malgn-noti",". 관리자단·API도 후속 검토 가능하나 1차는 사용자가 가장 자주 보는 사용자단에 두기로 결정(공개·blank 레이아웃).",[72,157,158,77,161,163,164,167],{},[40,159,160],{},"접근",[15,162,21],{}," 공개. ",[15,165,166],{},"definePageMeta({ layout: 'blank', auth: false })",". GNB·푸터 없는 단독 페이지 — 외부 공유에 그대로 쓸 수 있도록.",[72,169,170,173],{},[40,171,172],{},"두 산출물 운영",": MD가 정본, Vue 페이지는 동일 데이터를 살아있는 카탈로그로 노출. 어긋나면 MD 우선. Vue 페이지 내부 데이터는 정적 embed (별도 API 없음 — helper-pms처럼 R2 영속·자동 저장은 과한 인프라).",[72,175,176,179],{},[40,177,178],{},"5 단계 가중치",": 1 준비 10% · 2 정책 15% · 3 기획 20% · 4 디자인 10% · 5 개발 45% — 개발 비중이 큰 프로젝트라 Step 5를 45%로 가중. Step 1·2는 합의·문서 위주라 가볍게.",[72,181,182,185,186,189,190,193,194,197,198,201,202,205],{},[40,183,184],{},"Step 5 재구성 방침",": 원본 항목(채널·도메인 단위, 대부분 0%)을 실제 산출물 단위 5 그룹으로 재구성 — ",[15,187,188],{},"5-1 설계 및 준비","(7) \u002F ",[15,191,192],{},"5-2 API 서버","(16) \u002F ",[15,195,196],{},"5-3 사용자단 화면","(15) \u002F ",[15,199,200],{},"5-4 관리자단 화면","(13) \u002F ",[15,203,204],{},"5-5 통합·배포","(7).",[72,207,208,77,211,214,215,218,219,222,223,226],{},[40,209,210],{},"상태 매핑",[15,212,213],{},"done"," ✅ \u002F ",[15,216,217],{},"in_progress"," 🟢 \u002F ",[15,220,221],{},"pending"," ⚪ \u002F ",[15,224,225],{},"blocked"," ⛔. Vue에서는 색칩 + 점 + 진척률 막대 색상으로 동시 표현.",[24,228,230],{"id":229},"_3-코드-변경","3. 코드 변경",[232,233,235,236,238],"h3",{"id":234},"_31-docwbsmd-텍스트-정본-신규","3.1 ",[15,237,17],{}," — 텍스트 정본 (신규)",[29,240,241,244],{},[79,242,17],{"href":243},"..\u002FWBS"," — 약 220 라인. 구조:",[246,247,248,257,263,269,275,281,287,300,306],"ol",{},[72,249,250,253,254,63],{},[40,251,252],{},"진행률 스냅샷 (2026-06-01)"," — 5 단계 × 가중치·진행률·핵심 진행 사항. 가중평균 계산식 명시(",[15,255,256],{},"0.10×55 + 0.15×55 + 0.20×35 + 0.10×20 + 0.45×55 ≈ 44.5",[72,258,259,262],{},[40,260,261],{},"상태 범례",".",[72,264,265,268],{},[40,266,267],{},"단계별 가중치"," 표.",[72,270,271,274],{},[40,272,273],{},"Step 1 — 프로젝트 준비 (10%)"," — 5 하위 섹션(R&R·사업기획 \u002F 사업준비 \u002F 커뮤니케이션 \u002F 서비스 메타 \u002F 환경 셋팅), 18 작업.",[72,276,277,280],{},[40,278,279],{},"Step 2 — 주요 서비스 정책 이슈 정리 (15%)"," — 6 하위 섹션(프로토타입 \u002F 주요 서비스 참조 \u002F 캠페인 \u002F 회원·결제·계약 \u002F 메시지 채널 정책 \u002F 캠페인·주소록·브랜드), 22 작업.",[72,282,283,286],{},[40,284,285],{},"Step 3 — 서비스 기획 (20%)"," — 3 하위 섹션(Front \u002F BackOffice 1차 \u002F BackOffice 2차), 22 작업.",[72,288,289,292,293,108,296,299],{},[40,290,291],{},"Step 4 — 디자인 \u002F 퍼블리싱 (10%)"," — 2 작업 + \"현재는 개발 측 ",[15,294,295],{},"doc\u002FDESIGN.md",[15,297,298],{},"\u002Fguide"," 카탈로그로 대체 운영\" 주석.",[72,301,302,305],{},[40,303,304],{},"Step 5 — 서비스 개발 (45%)"," — ⚠️ 재구성 마커 + 5 하위 섹션(설계 및 준비 7 \u002F API 서버 16 \u002F 사용자단 화면 15 \u002F 관리자단 화면 13 \u002F 통합·배포 7), 총 58 작업.",[72,307,308,311],{},[40,309,310],{},"알려진 한계 \u002F 다음 단계"," — DDL 보류·백엔드 연동 미·관리자단 페이지 미·NHN real\u002FPG\u002FAI 게이트웨이 미정·시스템 페이지 재작업·Step 4 정식 산출물 미.",[232,313,315,316,318],{"id":314},"_32-apppageswbsvue-공개-라이브-카탈로그-신규","3.2 ",[15,317,53],{}," — 공개 라이브 카탈로그 (신규)",[29,320,321,324],{},[79,322,53],{"href":323},"..\u002F..\u002Fapp\u002Fpages\u002Fwbs.vue"," — 약 650 라인 (스크립트 280 + 템플릿 130 + 스타일 240).",[69,326,327,340,543,574,584,602,612,626,632],{},[72,328,329,100,332,335,336,339],{},[40,330,331],{},"definePageMeta",[15,333,334],{},"{ layout: 'blank', auth: false }",". ",[15,337,338],{},"\u002Fhelp","와 동일한 단독 셸 패턴.",[72,341,342,345,346],{},[40,343,344],{},"데이터 형태",":\n",[347,348,353],"pre",{"className":349,"code":350,"language":351,"meta":352,"style":352},"language-ts shiki shiki-themes github-light github-dark","type Status = 'done' | 'in_progress' | 'pending' | 'blocked'\ninterface Task { id, group?, title, status, owner, note?, targetDate?, completionDate?, href? }\ninterface Stage { id, no, emoji, name, summary, weight, progress, tasks }\nconst STAGES: Stage[] = [\u002F* Step 1~5, 113 tasks *\u002F]\n","ts","",[15,354,355,391,463,512],{"__ignoreMap":352},[356,357,360,364,368,371,375,378,381,383,386,388],"span",{"class":358,"line":359},"line",1,[356,361,363],{"class":362},"szBVR","type",[356,365,367],{"class":366},"sScJk"," Status",[356,369,370],{"class":362}," =",[356,372,374],{"class":373},"sZZnC"," 'done'",[356,376,377],{"class":362}," |",[356,379,380],{"class":373}," 'in_progress'",[356,382,377],{"class":362},[356,384,385],{"class":373}," 'pending'",[356,387,377],{"class":362},[356,389,390],{"class":373}," 'blocked'\n",[356,392,394,397,400,404,408,411,414,417,419,422,424,427,429,432,434,437,439,441,444,446,448,451,453,455,458,460],{"class":358,"line":393},2,[356,395,396],{"class":362},"interface",[356,398,399],{"class":366}," Task",[356,401,403],{"class":402},"sVt8B"," { ",[356,405,407],{"class":406},"s4XuR","id",[356,409,410],{"class":402},", ",[356,412,413],{"class":406},"group",[356,415,416],{"class":362},"?",[356,418,410],{"class":402},[356,420,421],{"class":406},"title",[356,423,410],{"class":402},[356,425,426],{"class":406},"status",[356,428,410],{"class":402},[356,430,431],{"class":406},"owner",[356,433,410],{"class":402},[356,435,436],{"class":406},"note",[356,438,416],{"class":362},[356,440,410],{"class":402},[356,442,443],{"class":406},"targetDate",[356,445,416],{"class":362},[356,447,410],{"class":402},[356,449,450],{"class":406},"completionDate",[356,452,416],{"class":362},[356,454,410],{"class":402},[356,456,457],{"class":406},"href",[356,459,416],{"class":362},[356,461,462],{"class":402}," }\n",[356,464,466,468,471,473,475,477,480,482,485,487,490,492,495,497,500,502,505,507,510],{"class":358,"line":465},3,[356,467,396],{"class":362},[356,469,470],{"class":366}," Stage",[356,472,403],{"class":402},[356,474,407],{"class":406},[356,476,410],{"class":402},[356,478,479],{"class":406},"no",[356,481,410],{"class":402},[356,483,484],{"class":406},"emoji",[356,486,410],{"class":402},[356,488,489],{"class":406},"name",[356,491,410],{"class":402},[356,493,494],{"class":406},"summary",[356,496,410],{"class":402},[356,498,499],{"class":406},"weight",[356,501,410],{"class":402},[356,503,504],{"class":406},"progress",[356,506,410],{"class":402},[356,508,509],{"class":406},"tasks",[356,511,462],{"class":402},[356,513,515,518,522,525,527,530,533,536,540],{"class":358,"line":514},4,[356,516,517],{"class":362},"const",[356,519,521],{"class":520},"sj4cs"," STAGES",[356,523,524],{"class":362},":",[356,526,470],{"class":366},[356,528,529],{"class":402},"[] ",[356,531,532],{"class":362},"=",[356,534,535],{"class":402}," [",[356,537,539],{"class":538},"sJ8bj","\u002F* Step 1~5, 113 tasks *\u002F",[356,541,542],{"class":402},"]\n",[72,544,545,77,548,550,551,554,555,558,559,108,562,565,566,569,570,573],{},[40,546,547],{},"헤더",[15,549,338],{}," 패턴 차용 — ",[15,552,553],{},"position: sticky"," 56px 높이 + ",[15,556,557],{},"\u003CAppLogoMark\u002F>"," 로고 + ",[15,560,561],{},"wbs-header-divider",[15,563,564],{},"WBS"," crumb + ",[15,567,568],{},"맑은 메시징 프로젝트 작업 내역"," 타이틀 + 우측 ",[15,571,572],{},"doc\u002FWBS.md ↗"," 외부 링크.",[72,575,576,579,580,583],{},[40,577,578],{},"Title row"," — ",[15,581,582],{},"맑은 메시징"," h1(30px·600·-0.01em) + 부제(서비스 한 줄 설명 + 마지막 현행화 날짜).",[72,585,586,589,590,593,594,597,598,601],{},[40,587,588],{},"Hero stats"," (4-col grid) — ",[15,591,592],{},"전체 진행률","(가중평균 % + 36px tabular-nums + 너비 = 진행률인 검정 막대) span-2 + ",[15,595,596],{},"완료","(N\u002F총 작업 수) + ",[15,599,600],{},"진행 중","(N).",[72,603,604,607,608,611],{},[40,605,606],{},"단계별 진행률 오버뷰"," — 카드형 ul, 행 클릭 시 ",[15,609,610],{},"scrollToStage"," smooth scroll. 6-col grid(이모지·번호·이름\u002F요약·작업 수·진척률 막대+%·화살표).",[72,613,614,617,618,621,622,625],{},[40,615,616],{},"Stage 상세"," — 단계마다 head(이모지·이름·ID + 비중·진척률) + 작은 진척률 막대 + ",[15,619,620],{},"groupedTasks(stage)","로 그룹 카드 분할 렌더링. 각 작업 행은 ",[15,623,624],{},"task-id","(JetBrains Mono) + 상태 점 + 제목 + 외부 링크 아이콘 + 메모 + 우측(상태칩·담당자·목표→완료 날짜).",[72,627,628,631],{},[40,629,630],{},"반응형"," — 720px 미만에서 오버뷰 행과 작업 행 그리드를 단순화.",[72,633,634,637],{},[40,635,636],{},"푸터"," — 상단 1px border + 브랜드 + 카피.",[232,639,641],{"id":640},"_33-데이터-채우기","3.3 데이터 채우기",[69,643,644,650,659,671],{},[72,645,646,649],{},[40,647,648],{},"Step 1·2"," — 스크린샷 그대로 옮김.",[72,651,652,655,656,658],{},[40,653,654],{},"Step 3"," — 스크린샷 그대로 옮김. 운영가이드 메모에 \"(사용자단 ",[15,657,338],{}," 라이브 — 컨텐츠 보강 필요)\" 부기.",[72,660,661,664,665,667,668,670],{},[40,662,663],{},"Step 4"," — 스크린샷 그대로 옮기되 메모로 \"(개발 측에서 ",[15,666,295],{}," Relay-inspired v1.0 + ",[15,669,298],{}," 카탈로그로 대체 운영)\" 부기.",[72,672,673,676,677],{},[40,674,675],{},"Step 5"," — 사용자 요청대로 재구성:\n",[69,678,679,682,685,688,691],{},[72,680,681],{},"5-1 설계 및 준비 — 아키텍처·데이터 모델링·DS·사용자단\u002F관리자단 가이드·관리자단 셸·페이지 기획 MD 33종 ✅ 7건.",[72,683,684],{},"5-2 API 서버 — Workers 부트스트랩·DB 49 테이블·기초 CRUD 14·OpenAPI 37·인증·발송 5채널·멱등성·NHN 어댑터 5·Queues + Consumer ✅ 9건 \u002F Webhook 핸들러·Export·Flow 🟢 3건 \u002F 캠페인·PG·AI·NHN real ⚪ 4건 = 16건.",[72,686,687],{},"5-3 사용자단 화면 — 인증\u002F계정·발송 6채널·이력 5+stats·주소록·발신정보 6·템플릿 5+settings·캠페인·크레딧\u002F결제·문의·나의 페이지·랜딩페이지·공개 랜딩·디자인 가이드 ✅ 13건 \u002F 시스템 페이지 🟢 1 \u002F 백엔드 연동 ⚪ 1 = 15건.",[72,689,690],{},"5-4 관리자단 화면 — 셸·기획 MD ✅ 2건 \u002F P0·P1·P2 ⚪ 11건 = 13건.",[72,692,693],{},"5-5 통합·배포 — 사용자단 Pages 🟢 \u002F 관리자단 Pages ✅ \u002F API Workers ✅ \u002F DDL ⛔ \u002F NHN real·PG·AI ⚪ = 7건.",[24,695,697],{"id":696},"_4-배포-47-사용자단","4. 배포 #47 (사용자단)",[69,699,700,722,727,733],{},[72,701,702,705,706,709,710,713,714,717,718,721],{},[15,703,704],{},"pnpm build"," → Nitro ",[15,707,708],{},"cloudflare-pages"," 프리셋 → ",[15,711,712],{},"dist\u002F_worker.js"," 빌드 OK. ",[15,715,716],{},"wbs-BWsapYCM.mjs"," 30.3 kB \u002F ",[15,719,720],{},"wbs-styles.BOjKIqTn.mjs"," 29.9 kB 청크 생성. Total 3.02 MB(903 kB gzip).",[72,723,724,262],{},[15,725,726],{},"npx wrangler@4 pages deploy dist --project-name=malgn-noti --branch=main --commit-dirty=true --commit-message \"deploy wbs page\"",[72,728,729,730,262],{},"alias ",[15,731,732],{},"https:\u002F\u002F0ecc825e.malgn-noti.pages.dev",[72,734,735,736,739,740,742,743,746,747,749,750,753],{},"검증: ",[15,737,738],{},"GET https:\u002F\u002Fmalgn-noti.pages.dev\u002Fwbs"," → HTTP 200. 그렙으로 ",[15,741,568],{}," 헤더·",[15,744,745],{},"wbs-header-title"," 2·",[15,748,592],{}," 1·",[15,751,752],{},"stage-emoji"," 6 출력 확인(5 stages × 본문 + 1 hero\u002Foverview).",[24,755,756],{"id":756},"산출물",[69,758,759,764,777],{},[72,760,761],{},[15,762,763],{},"malgn-noti: WBS — doc\u002FWBS.md 정본 + \u002Fwbs 공개 라이브 카탈로그 (배포 #47)",[72,765,766,767],{},"신규 파일:\n",[69,768,769,773],{},[72,770,771],{},[79,772,17],{"href":243},[72,774,775],{},[79,776,53],{"href":323},[72,778,779,780,784,785,262],{},"프로덕션: ",[79,781,782],{"href":782,"rel":783},"https:\u002F\u002Fmalgn-noti.pages.dev\u002Fwbs",[83]," · alias ",[15,786,787],{},"https:\u002F\u002F0ecc825e.malgn-noti.pages.dev\u002Fwbs",[24,789,791],{"id":790},"다음-단계-알려진-한계","다음 단계 \u002F 알려진 한계",[69,793,794,800,806,821,827,838],{},[72,795,796,799],{},[40,797,798],{},"MD ↔ Vue 동기화는 수동."," 가중치·진척률·작업 상태가 어긋나면 한 곳만 갱신해 두기 쉬움 — 큰 변화가 생기면 두 파일을 같이 수정하는 디스플린 유지.",[72,801,802,805],{},[40,803,804],{},"진척률은 추정."," 실제 작업 수 대비 ✅\u002F🟢\u002F⚪\u002F⛔ 비율을 기준으로 추정 — 객관 지표(완료 PR 수·테스트 통과율 등) 도입 시 더 정확히.",[72,807,808,100,813,816,817,820],{},[40,809,810,812],{},[15,811,21],{}," 인덱싱 차단 미비.",[15,814,815],{},"nuxt.config.ts head","에 전체 ",[15,818,819],{},"noindex,nofollow"," 설정이 있는지 재확인 필요(검색 노출 방지).",[72,822,823,826],{},[40,824,825],{},"외부 자료 링크 일부 placeholder."," 컨설팅팀\u002F디자인팀 산출물(단가표·계약서·디자인 스타일 가이드 등)의 실제 URL이 정해지면 MD·Vue 양쪽에 채워 넣기.",[72,828,829,832,833,108,835,837],{},[40,830,831],{},"Step 4 정식 산출물."," 디자인팀의 정식 디자인 스타일 가이드 + 퍼블리싱 MD는 여전히 미. 개발 측 ",[15,834,295],{},[15,836,298],{}," 카탈로그로 대체 운영 중.",[72,839,840,843,844,847,848,108,851,854],{},[40,841,842],{},"자동화 가능성",": helper-pms처럼 R2 영속 + 자동 저장 형태로 운영하고 싶다면, ",[15,845,846],{},"malgn-noti-api","에 ",[15,849,850],{},"GET \u002Fwbs",[15,852,853],{},"PUT \u002Fwbs"," 추가 → 본 페이지를 편집 가능 페이지로 전환. 1차에서는 스코프에서 제외.",[856,857],"hr",{},[10,859,861,862,864],{"id":860},"_2-0002_export_flowsql-ddl-라이브-이미-적용-확인-라이브-검증-sql-파일-동기화","§2. ",[15,863,138],{}," DDL — 라이브 이미 적용 확인 + 라이브 검증 + SQL 파일 동기화",[24,866,868],{"id":867},"한-줄","한 줄",[29,870,871,874,875,878,879,882,883,886,887,889,890,893,894,897,898,901,902,904],{},[15,872,873],{},"wrangler dev --remote","의 1105 잔류로 ",[15,876,877],{},"\u002Fadmin\u002Fmigrate"," 경로가 막혀 있던 상태에서, Aurora 직결(",[15,880,881],{},"noti"," 계정 + SSL REQUIRED)로 들어가 ",[40,884,885],{},"4 신규 테이블(TB_EXPORT_JOB \u002F TB_FLOW_DEFINITION \u002F TB_FLOW_RUN \u002F TB_FLOW_STEP_RUN)이 이미 적용돼 있음","을 확인. 컬럼은 우리 ",[15,888,138],{},"과 100% 일치, ",[40,891,892],{},"인덱스·FK는 라이브 쪽이 더 정교","(FK 6개 + 의미 있는 인덱스명) — 출처는 사전 작업으로 추정. 라이브 워커의 ",[15,895,896],{},"\u002Fexport-jobs","·",[15,899,900],{},"\u002Fflow-definitions"," GET\u002FPOST 4건 모두 200\u002F201 정상 응답으로 e2e 확인. 검증 과정에서 생긴 테스트 데이터(임시 user\u002Fcompany 2건 + export_job\u002Fflow_def 각 1건)를 즉시 cleanup해 빈 상태 복구. ",[15,903,138],{},"을 라이브 정본(인덱스·FK 포함)에 맞춰 갱신해 신규 환경에서도 동일하게 적용되도록 동기화.",[24,906,908],{"id":907},"_21-cloudflare-1105-재시도-3회-모두-실패","2.1 Cloudflare 1105 재시도 (3회) → 모두 실패",[910,911,912,931],"table",{},[913,914,915],"thead",{},[916,917,918,922,925,928],"tr",{},[919,920,921],"th",{},"시도",[919,923,924],{},"시각 (UTC)",[919,926,927],{},"Ray ID",[919,929,930],{},"결과",[932,933,934,954,970],"tbody",{},[916,935,936,940,943,948],{},[937,938,939],"td",{},"1차",[937,941,942],{},"01:47",[937,944,945],{},[15,946,947],{},"a04a8ca2dd1125d4",[937,949,950,951],{},"HTTP 503 — ",[15,952,953],{},"Error 1105 Temporarily unavailable",[916,955,956,959,962,967],{},[937,957,958],{},"2차",[937,960,961],{},"01:54",[937,963,964],{},[15,965,966],{},"a04a9753cbaedd38",[937,968,969],{},"HTTP 503 — 동일",[916,971,972,975,978,981],{},[937,973,974],{},"3차",[937,976,977],{},"02:24~02:28",[937,979,980],{},"—",[937,982,983],{},"HTTP 503 12회 폴링 모두 (10초 간격)",[69,985,986,1007],{},[72,987,988,989,991,992,995,996,897,999,1002,1003,1006],{},"모두 ",[15,990,873],{},"가 띄운 임시 edge-preview 워커에서 발생. 라이브 워커(",[15,993,994],{},"https:\u002F\u002Fmalgn-noti-api.malgnsoft.workers.dev",")는 ",[15,997,998],{},"\u002Fhealth",[15,1000,1001],{},"\u002Fhealth\u002Fdb"," 200으로 영향 없음 → ",[40,1004,1005],{},"1105는 Cloudflare 측 dev\u002Fpreview 인프라 한정 장애","로 확정.",[72,1008,1009],{},"결정: 한 번만 쓸 카드인 일회용 Aurora SG whitelist 경로로 우회. 운영 정책 갱신(Cloudflare Tunnel·RDS Proxy·bastion 등)은 별도 후속 작업으로 분리.",[24,1011,1013],{"id":1012},"_22-hyperdrive-aurora-라이브-연결-정상성-사전-확인","2.2 Hyperdrive ↔ Aurora 라이브 연결 정상성 사전 확인",[69,1015,1016,1033,1045,1062,1076],{},[72,1017,1018,1021,1022,1025,1026,1028,1029,1032],{},[15,1019,1020],{},"wrangler hyperdrive get a2ba4efe7421464da1d5ff5e620b33a3"," — 설정 정상 (origin: ",[15,1023,1024],{},"malgn-dev-db.cluster-c53h9wjjbjbr.ap-northeast-2.rds.amazonaws.com:3306"," \u002F db ",[15,1027,881],{}," \u002F user ",[15,1030,1031],{},"admin"," \u002F SSL REQUIRED \u002F connection_limit 60 \u002F 캐싱 활성).",[72,1034,1035,1037,1038,410,1041,1044],{},[15,1036,1001],{}," 3회 — 모두 200, ",[15,1039,1040],{},"mysql_version: 8.0.42",[40,1042,1043],{},"cold 512ms → warm 355ms → warm 360ms"," (Hyperdrive 캐시 효과 뚜렷).",[72,1046,1047,1048,897,1051,897,1054,1057,1058,1061],{},"보호 라우트 가드 — ",[15,1049,1050],{},"\u002Fme",[15,1052,1053],{},"\u002Fcontacts",[15,1055,1056],{},"\u002Fdispatch\u002Frequests"," 모두 401 (DB 단계 진입 전 차단). ",[15,1059,1060],{},"\u002Fadmin\u002F*"," 404 (프로덕션 가드 정상).",[72,1063,1064,1065,1068,1069,1071,1072,1075],{},"실 DB write+read — ",[15,1066,1067],{},"\u002Fauth\u002Fsignup","(임시) → JWT 169자 → ",[15,1070,1050],{}," SELECT 2회 → ",[40,1073,1074],{},"249ms 응답"," + 컬럼 정상 매핑.",[72,1077,1078],{},"결론: 1105와 무관하게 라이브 인프라는 한 통으로 살아 있음.",[24,1080,1082],{"id":1081},"_23-aurora-직결-tcp-도달성-mysql-인증","2.3 Aurora 직결 — TCP 도달성 + mysql 인증",[69,1084,1085,1095,1101,1112],{},[72,1086,1087,1088,1090,1091,1094],{},"사용자 제공 — ",[15,1089,881],{}," 계정 + 패스워드(채팅 외부에 기록 안 함, ",[15,1092,1093],{},"MYSQL_PWD"," 환경변수로만 1회 쉘에서 사용).",[72,1096,1097,1098,262],{},"내 outbound IP — ",[15,1099,1100],{},"211.119.233.35",[72,1102,1103,1104,1107,1108,1111],{},"TCP probe: ",[15,1105,1106],{},"nc -zv -G 5 malgn-dev-db.cluster-...:3306"," → ",[40,1109,1110],{},"즉시 connection succeeded"," — SG 인바운드가 이미 열려 있는 상태(추정: 사전에 noti IP 또는 관련 대역이 화이트리스트됨).",[72,1113,1114,1115,1118,1119,1122,1123,1125],{},"mysql 접속(",[15,1116,1117],{},"--ssl-mode=REQUIRED","): ",[15,1120,1121],{},"noti@%",", DB ",[15,1124,881],{},", 서버 8.0.42 → ✅.",[24,1127,1129],{"id":1128},"_24-사전-ddl-적용-확인-컬럼-100-일치-인덱스fk-더-풍부","2.4 사전 DDL 적용 확인 — 컬럼 100% 일치, 인덱스·FK 더 풍부",[69,1131,1132,1139,1149,1201],{},[72,1133,1134,1135,1138],{},"전체 TB_ 카운트: ",[40,1136,1137],{},"50"," (= 49 initial + 1 idempotency + 4 신규 − 4 중복? 아님. 0000_initial.sql의 정확 적용본은 45 + 0001 1 + 0002 4 = 50.) — 4 신규가 50 안에 이미 포함돼 있음.",[72,1140,1141,1142,1144,1145,1148],{},"4 신규 테이블 컬럼 — ",[15,1143,138],{},"(2026-05-31자 초안)과 ",[40,1146,1147],{},"모두 일치",": 데이터 타입·NULL 여부·기본값·자동 타임스탬프 모두 동일.",[72,1150,1151,345,1154],{},[40,1152,1153],{},"인덱스\u002FFK는 라이브 쪽이 더 정교",[69,1155,1156,1170,1183,1192],{},[72,1157,1158,1161,1162,1165,1166,1169],{},[15,1159,1160],{},"TB_EXPORT_JOB",": 라이브 ",[15,1163,1164],{},"idx_export_company_state(company_id, job_state, requested_at) + idx_export_user(user_id, requested_at) + FK fk_export_company → TB_COMPANY + FK fk_export_user → TB_USER",". 초안 안은 단일 ",[15,1167,1168],{},"idx_export_company_user(company_id, user_id, requested_at) + idx_export_state(job_state, requested_at)","만 있었음.",[72,1171,1172,1161,1175,1178,1179,1182],{},[15,1173,1174],{},"TB_FLOW_DEFINITION",[15,1176,1177],{},"idx_flowdef_company_status(company_id, status, created_at) + FK fk_flowdef_company → TB_COMPANY",". 초안은 ",[15,1180,1181],{},"idx_flow_def_company(company_id, created_at)","만.",[72,1184,1185,1161,1188,1191],{},[15,1186,1187],{},"TB_FLOW_RUN",[15,1189,1190],{},"idx_flowrun_company_state(company_id, run_state, started_at) + FK fk_flowrun_company → TB_COMPANY + FK fk_flowrun_def → TB_FLOW_DEFINITION + 보조 키 fk_flowrun_def",". 초안은 인덱스 2개만, FK 없음.",[72,1193,1194,1161,1197,1200],{},[15,1195,1196],{},"TB_FLOW_STEP_RUN",[15,1198,1199],{},"idx_fsr_run(flow_run_id, node_order) + idx_fsr_dispatch(dispatch_request_id) + FK fk_fsr_run → TB_FLOW_RUN",". 초안은 단일 인덱스만, FK 없음.",[72,1202,1203,1204,1207,1208,262],{},"4 테이블 모두 행 수 ",[40,1205,1206],{},"0"," → 빈 신규 생성. ",[40,1209,1210],{},"즉, 출처는 사전 작업(SG whitelist + 직결 또는 다른 운영 경로)",[24,1212,1214],{"id":1213},"_25-라이브-워커-e2e-검증-4-호출-모두-통과","2.5 라이브 워커 e2e 검증 (4 호출 모두 통과)",[910,1216,1217,1226],{},[913,1218,1219],{},[916,1220,1221,1224],{},[919,1222,1223],{},"호출",[919,1225,930],{},[932,1227,1228,1243,1264,1276],{},[916,1229,1230,1236],{},[937,1231,1232,1235],{},[15,1233,1234],{},"GET \u002Fexport-jobs"," (auth)",[937,1237,1238,1239,1242],{},"200 — ",[15,1240,1241],{},"{data:[], nextCursor:null}"," · 449ms",[916,1244,1245,1253],{},[937,1246,1247,100,1250],{},[15,1248,1249],{},"POST \u002Fexport-jobs",[15,1251,1252],{},"{resourceType:\"history_sms\", params:{from,to}}",[937,1254,1255,1256,1259,1260,1263],{},"201 — id=1 \u002F ",[15,1257,1258],{},"jobState:\"pending\""," \u002F ",[15,1261,1262],{},"expires_at"," 등록 +30일 자동 계산 · 306ms",[916,1265,1266,1271],{},[937,1267,1268,1235],{},[15,1269,1270],{},"GET \u002Fflow-definitions",[937,1272,1238,1273,1275],{},[15,1274,1241],{}," · 400ms",[916,1277,1278,1284],{},[937,1279,1280,1283],{},[15,1281,1282],{},"POST \u002Fflow-definitions"," (alimtalk→sms on_fail 5분 폴백)",[937,1285,1286,1287,897,1290,1293,1294,1297],{},"201 — id=1 \u002F nodes JSON 보존 \u002F ",[15,1288,1289],{},"createdAt",[15,1291,1292],{},"updatedAt"," 자동 \u002F ",[15,1295,1296],{},"deletedAt:null"," · 563ms",[29,1299,1300,1301,1304],{},"→ ",[40,1302,1303],{},"라이브 워커 + 4 신규 라우트 + 라이브 DB","가 한 통으로 정상. CRUD ✅. 처리 worker \u002F 실행 엔진은 여전히 미.",[24,1306,1308],{"id":1307},"_26-테스트-데이터-cleanup","2.6 테스트 데이터 cleanup",[69,1310,1311,1344,1347,1363],{},[72,1312,1313,1314,1317,1318,897,1321,1324,1325,1328,1329,897,1332,1324,1335,1337,1338,1337,1340,1343],{},"검증 과정에서 생성: ",[15,1315,1316],{},"TB_USER","(loginid ",[15,1319,1320],{},"hd-check-…",[15,1322,1323],{},"ddl-…",") 2건 \u002F ",[15,1326,1327],{},"TB_COMPANY","(name ",[15,1330,1331],{},"hyperdrive-check-…",[15,1333,1334],{},"ddl-live-check-…",[15,1336,1160],{}," id=1 \u002F ",[15,1339,1174],{},[15,1341,1342],{},"TB_TERMS_AGREEMENT"," 0건(현재 약관 미배포로 자동 생성 없음).",[72,1345,1346],{},"단일 트랜잭션 묶음 없이 순서대로 DELETE — FK 제약을 만족하도록 자식 → 부모 순.",[72,1348,1349,1350,1259,1353,1259,1356,1259,1359,1362],{},"사후 카운트: ",[15,1351,1352],{},"TB_EXPORT_JOB=0",[15,1354,1355],{},"TB_FLOW_DEFINITION=0",[15,1357,1358],{},"leftover_users=0",[15,1360,1361],{},"leftover_companies=0"," ✅.",[72,1364,1365,1366,1259,1369,1372],{},"AUTO_INCREMENT 잔류: ",[15,1367,1368],{},"TB_EXPORT_JOB.AUTO_INCREMENT=2",[15,1370,1371],{},"TB_FLOW_DEFINITION.AUTO_INCREMENT=2"," (정상 — 다음 INSERT는 id=2부터 시작).",[24,1374,1376,1377,1379],{"id":1375},"_27-0002_export_flowsql-라이브-정본-동기화","2.7 ",[15,1378,138],{}," 라이브 정본 동기화",[69,1381,1382,1419,1422],{},[72,1383,1384,1385,1388,1389,1392,1393,897,1396,897,1399,897,1402,897,1405,897,1408,1411,1412,897,1415,1418],{},"초안 SQL 파일을 라이브 ",[15,1386,1387],{},"SHOW CREATE TABLE"," 결과 기준으로 갱신 — 인덱스명 변경(",[15,1390,1391],{},"idx_export_company_state"," 등) + FK 6개 추가(",[15,1394,1395],{},"fk_export_company",[15,1397,1398],{},"fk_export_user",[15,1400,1401],{},"fk_flowdef_company",[15,1403,1404],{},"fk_flowrun_company",[15,1406,1407],{},"fk_flowrun_def",[15,1409,1410],{},"fk_fsr_run",") + 코멘트 일치(",[15,1413,1414],{},"'history_sms, contacts 등'",[15,1416,1417],{},"'[{order, channel, template_id, condition, delay_minutes}]'"," 등).",[72,1420,1421],{},"CLAUDE.md §5 \"파티션 테이블 — FK 미사용\" 원칙은 유지 — 이번 4 테이블은 모두 비파티션이라 FK 적용 가능.",[72,1423,1424],{},"파일 헤더에 \"2026-06-01 현행화 — 라이브(Aurora) 정본과 동기화: FK 6개 + 의미 있는 인덱스명\" 명시.",[24,1426,1428],{"id":1427},"_28-산출물","2.8 산출물",[69,1430,1431,1437,1440,1447],{},[72,1432,1433,1436],{},[15,1434,1435],{},"malgn-noti-api: src\u002Fdb\u002Fmigrations\u002F0002_export_flow.sql"," — 라이브 정본 동기화 (1 file, 인덱스명 변경 + FK 6 추가 + 코멘트 일치).",[72,1438,1439],{},"라이브 DB — 변경 없음(이미 적용된 정본 그대로 + cleanup으로 빈 상태 복원).",[72,1441,1442,1443,1446],{},"라이브 Worker — 변경 없음(이미 배포 #8 ",[15,1444,1445],{},"95f9f894...","이 4 라우트를 정상 노출 중).",[72,1448,1449,1452],{},[15,1450,1451],{},"malgn-noti: doc\u002FWBS.md"," 갱신 — 5-2-11\u002F12 🟢 (CRUD ✅, 처리 worker \u002F 실행 엔진 미) \u002F 5-5-4 ⛔→✅ (DDL 적용 확인).",[24,1454,1456],{"id":1455},"_29-다음-단계-알려진-한계","2.9 다음 단계 \u002F 알려진 한계",[69,1458,1459,1480,1492,1501],{},[72,1460,1461,579,1464,1467,1468,1471,1472,1475,1476,1479],{},[40,1462,1463],{},"Drizzle schema.ts vs 라이브 인덱스\u002FFK",[15,1465,1466],{},"src\u002Fdb\u002Fschema.ts","의 export\u002Fflow 테이블 정의는 인덱스\u002FFK를 선언하지 않음(컬럼만). 런타임 동작에는 영향 없지만, ",[15,1469,1470],{},"drizzle-kit introspect"," 또는 schema에서 명시적으로 ",[15,1473,1474],{},"index()","\u002F",[15,1477,1478],{},"foreignKey()","를 선언해 정합화하는 게 위생적. 후속.",[72,1481,1482,579,1485,1487,1488,1491],{},[40,1483,1484],{},"운영 절차 갱신",[15,1486,873],{}," 1105 같은 dev\u002Fpreview 장애가 또 발생할 때를 대비, CLAUDE.md §12 후보 중 ",[40,1489,1490],{},"Cloudflare Tunnel(cloudflared) → Aurora"," 셋업 검토. 별도 작업.",[72,1493,1494,1497,1498,1500],{},[40,1495,1496],{},"SG 정책 재검토"," — 사전 작업에서 어떤 IP 대역이 화이트리스트됐는지 한 번 정리. ",[15,1499,881],{}," 계정의 권한 범위(CREATE TABLE 가능 여부)도 운영 문서화 필요.",[72,1502,1503,579,1506,1508,1509,1512,1513,897,1515,1517],{},[40,1504,1505],{},"처리 worker 미",[15,1507,896],{}," 처리 워커(R2 업로드 + presigned URL) \u002F ",[15,1510,1511],{},"\u002Fsend\u002Fflow"," 실행 엔진 + ",[15,1514,1187],{},[15,1516,1196],{}," 천이 — 둘 다 별도 마일스톤.",[856,1519],{},[10,1521,1523,1524,1527],{"id":1522},"_3-schemats-정합화-exportflow-4-테이블-인덱스fk-명시화","§3. ",[15,1525,1526],{},"schema.ts"," 정합화 — export\u002Fflow 4 테이블 인덱스·FK 명시화",[24,1529,868],{"id":1530},"한-줄-1",[29,1532,1533,1534,1536,1537,1543,1544,410,1547,1550,1551,1554,1555,1557],{},"§2에서 ",[15,1535,138],{}," 파일을 라이브 Aurora 정본에 맞춰 동기화했지만, ",[40,1538,1539,1540,1542],{},"Drizzle ORM의 ",[15,1541,1466],{},"는 컬럼만 정의되어 있고 인덱스\u002FFK는 0건"," — 코드↔라이브 drift 8건이 남아 있던 상태. 이를 라이브 정본 기준으로 명시화 (인덱스 6 + FK 6, 총 12 항목). ",[40,1545,1546],{},"컬럼 정의는 일절 변경하지 않음",[40,1548,1549],{},"다른 테이블은 손대지 않음","(서비스 중). 런타임 동작에 영향 0, Worker 재배포 불필요. typecheck 통과 + ",[15,1552,1553],{},"git diff"," 1 file +22 -4 — ",[15,1556,1526],{}," 외 변경 0 확인.",[24,1559,1561],{"id":1560},"_31-작업-범위-4-테이블만","3.1 작업 범위 — 4 테이블만",[29,1563,1564,1565,262],{},"원칙: \"다른 테이블은 현재 서비스 중이라 함부로 건들면 안 된다\" — export\u002Fflow 4 테이블 ",[40,1566,1567],{},"한정",[910,1569,1570,1583],{},[913,1571,1572],{},[916,1573,1574,1577,1580],{},[919,1575,1576],{},"테이블",[919,1578,1579],{},"추가 인덱스",[919,1581,1582],{},"추가 FK",[932,1584,1585,1614,1633,1657],{},[916,1586,1587,1593,1601],{},[937,1588,1589,1592],{},[15,1590,1591],{},"exportJob"," (TB_EXPORT_JOB)",[937,1594,1595,108,1598],{},[15,1596,1597],{},"idx_export_company_state(company_id, job_state, requested_at)",[15,1599,1600],{},"idx_export_user(user_id, requested_at)",[937,1602,1603,1107,1605,1608,1609,1107,1611],{},[15,1604,1395],{},[15,1606,1607],{},"company.id"," · ",[15,1610,1398],{},[15,1612,1613],{},"user.id",[916,1615,1616,1622,1627],{},[937,1617,1618,1621],{},[15,1619,1620],{},"flowDefinition"," (TB_FLOW_DEFINITION)",[937,1623,1624],{},[15,1625,1626],{},"idx_flowdef_company_status(company_id, status, created_at)",[937,1628,1629,1107,1631],{},[15,1630,1401],{},[15,1632,1607],{},[916,1634,1635,1641,1646],{},[937,1636,1637,1640],{},[15,1638,1639],{},"flowRun"," (TB_FLOW_RUN)",[937,1642,1643],{},[15,1644,1645],{},"idx_flowrun_company_state(company_id, run_state, started_at)",[937,1647,1648,1107,1650,1608,1652,1107,1654],{},[15,1649,1404],{},[15,1651,1607],{},[15,1653,1407],{},[15,1655,1656],{},"flowDefinition.id",[916,1658,1659,1665,1673],{},[937,1660,1661,1664],{},[15,1662,1663],{},"flowStepRun"," (TB_FLOW_STEP_RUN)",[937,1666,1667,108,1670],{},[15,1668,1669],{},"idx_fsr_run(flow_run_id, node_order)",[15,1671,1672],{},"idx_fsr_dispatch(dispatch_request_id)",[937,1674,1675,1107,1677],{},[15,1676,1410],{},[15,1678,1679],{},"flowRun.id",[29,1681,1682,1683,1686,1687,1689],{},"총 ",[40,1684,1685],{},"인덱스 6 + FK 6 = 12 항목",". 모두 라이브 ",[15,1688,1387],{}," 출력과 1:1 일치.",[24,1691,1693],{"id":1692},"_32-drizzle-문법","3.2 Drizzle 문법",[29,1695,1696,1697,1700,1701,1704,1705,1708,1709,1712],{},"각 ",[15,1698,1699],{},"mysqlTable(name, columns)"," 호출에 2번째 인자 콜백 ",[15,1702,1703],{},"(t) => ({...})","을 추가하는 방식. 컬럼은 그대로, 콜백에 ",[15,1706,1707],{},"index(...).on(...)"," 및 ",[15,1710,1711],{},"foreignKey({...})"," 선언.",[347,1714,1716],{"className":349,"code":1715,"language":351,"meta":352,"style":352},"export const exportJob = mysqlTable('TB_EXPORT_JOB', {\n  \u002F\u002F ... 컬럼 (변경 없음)\n}, t => ({\n  idxCompanyState: index('idx_export_company_state').on(t.companyId, t.jobState, t.requestedAt),\n  idxUser: index('idx_export_user').on(t.userId, t.requestedAt),\n  fkCompany: foreignKey({ name: 'fk_export_company', columns: [t.companyId], foreignColumns: [company.id] }),\n  fkUser: foreignKey({ name: 'fk_export_user', columns: [t.userId], foreignColumns: [user.id] }),\n}))\n",[15,1717,1718,1743,1748,1762,1783,1803,1821,1837],{"__ignoreMap":352},[356,1719,1720,1723,1726,1729,1731,1734,1737,1740],{"class":358,"line":359},[356,1721,1722],{"class":362},"export",[356,1724,1725],{"class":362}," const",[356,1727,1728],{"class":520}," exportJob",[356,1730,370],{"class":362},[356,1732,1733],{"class":366}," mysqlTable",[356,1735,1736],{"class":402},"(",[356,1738,1739],{"class":373},"'TB_EXPORT_JOB'",[356,1741,1742],{"class":402},", {\n",[356,1744,1745],{"class":358,"line":393},[356,1746,1747],{"class":538},"  \u002F\u002F ... 컬럼 (변경 없음)\n",[356,1749,1750,1753,1756,1759],{"class":358,"line":465},[356,1751,1752],{"class":402},"}, ",[356,1754,1755],{"class":406},"t",[356,1757,1758],{"class":362}," =>",[356,1760,1761],{"class":402}," ({\n",[356,1763,1764,1767,1770,1772,1775,1777,1780],{"class":358,"line":514},[356,1765,1766],{"class":402},"  idxCompanyState: ",[356,1768,1769],{"class":366},"index",[356,1771,1736],{"class":402},[356,1773,1774],{"class":373},"'idx_export_company_state'",[356,1776,63],{"class":402},[356,1778,1779],{"class":366},"on",[356,1781,1782],{"class":402},"(t.companyId, t.jobState, t.requestedAt),\n",[356,1784,1786,1789,1791,1793,1796,1798,1800],{"class":358,"line":1785},5,[356,1787,1788],{"class":402},"  idxUser: ",[356,1790,1769],{"class":366},[356,1792,1736],{"class":402},[356,1794,1795],{"class":373},"'idx_export_user'",[356,1797,63],{"class":402},[356,1799,1779],{"class":366},[356,1801,1802],{"class":402},"(t.userId, t.requestedAt),\n",[356,1804,1806,1809,1812,1815,1818],{"class":358,"line":1805},6,[356,1807,1808],{"class":402},"  fkCompany: ",[356,1810,1811],{"class":366},"foreignKey",[356,1813,1814],{"class":402},"({ name: ",[356,1816,1817],{"class":373},"'fk_export_company'",[356,1819,1820],{"class":402},", columns: [t.companyId], foreignColumns: [company.id] }),\n",[356,1822,1824,1827,1829,1831,1834],{"class":358,"line":1823},7,[356,1825,1826],{"class":402},"  fkUser: ",[356,1828,1811],{"class":366},[356,1830,1814],{"class":402},[356,1832,1833],{"class":373},"'fk_export_user'",[356,1835,1836],{"class":402},", columns: [t.userId], foreignColumns: [user.id] }),\n",[356,1838,1840],{"class":358,"line":1839},8,[356,1841,1842],{"class":402},"}))\n",[29,1844,1845,1846,410,1848,1850],{},"import에 ",[15,1847,1811],{},[15,1849,1769],{}," 2개 추가 (drizzle-orm\u002Fmysql-core).",[24,1852,1854],{"id":1853},"_33-효과","3.3 효과",[69,1856,1857,1866,1872,1878,1887],{},[72,1858,1859,1865],{},[40,1860,1861,1864],{},[15,1862,1863],{},"drizzle-kit introspect\u002Fgenerate"," drift 해소"," — 라이브와 코드 사이 인덱스\u002FFK 12건 불일치가 0으로.",[72,1867,1868,1871],{},[40,1869,1870],{},"신규 마이그레이션 안전"," — 누가 schema.ts 기준으로 새 마이그레이션 만들 때 \"FK·인덱스 DROP\" SQL이 생성되는 사고 방지.",[72,1873,1874,1877],{},[40,1875,1876],{},"신규 환경 부트스트랩 일관성"," — 새 환경에서 schema.ts → 마이그레이션 → DB 적용 시 동일한 정합 보장.",[72,1879,1880,1883,1884,1886],{},[40,1881,1882],{},"PR 가독성"," — \"이 테이블에 어떤 인덱스가 있는가?\" 답이 한 곳(",[15,1885,1526],{},")에 모임.",[72,1888,1889,1892],{},[40,1890,1891],{},"타입 안전성"," — FK 명시로 join 쿼리 작성 시 관계 자동 추론.",[24,1894,1896],{"id":1895},"_34-안전-가드","3.4 안전 가드",[69,1898,1899,1905,1917,1926,1932],{},[72,1900,1901,1904],{},[40,1902,1903],{},"컬럼 정의 변경 0"," — 1번째 인자 객체는 1바이트도 안 건드림.",[72,1906,1907,579,1910,1913,1914,1916],{},[40,1908,1909],{},"다른 테이블 변경 0",[15,1911,1912],{},"git diff --name-only","로 ",[15,1915,1466],{}," 단일 파일만 변경됨을 사전 확인.",[72,1918,1919,579,1922,1925],{},[40,1920,1921],{},"typecheck 통과",[15,1923,1924],{},"pnpm typecheck"," (tsc --noEmit) 에러 0.",[72,1927,1928,1931],{},[40,1929,1930],{},"런타임 영향 0"," — Drizzle 쿼리 빌더는 이 콜백을 마이그레이션\u002F툴체인 단에서만 사용. 런타임 DML\u002FSELECT에 변화 없음.",[72,1933,1934,1937,1938,1940],{},[40,1935,1936],{},"Worker 재배포 불필요"," — 배포 #8(",[15,1939,1445],{},") 그대로 라이브 정상.",[24,1942,1944],{"id":1943},"_35-산출물","3.5 산출물",[69,1946,1947,1953],{},[72,1948,1949,1952],{},[15,1950,1951],{},"malgn-noti-api: 0475bd2 db(schema): export\u002Fflow 4 테이블 인덱스·FK 명시화 (라이브 정합)"," (1 file, +22 -4).",[72,1954,1955],{},"라이브 DB·Worker — 변경 없음.",[24,1957,1959],{"id":1958},"_36-다음-단계-알려진-한계","3.6 다음 단계 \u002F 알려진 한계",[69,1961,1962,1968],{},[72,1963,1964,1967],{},[40,1965,1966],{},"49 테이블 전체 점검은 별도 작업"," — schema.ts의 나머지 46 테이블도 동일하게 인덱스\u002FFK 미선언 가능성 큼. 단 서비스 중이라 한 번에 큰 정합 작업은 위험. 추후 별도 마일스톤에서 테이블별로 점진적 점검.",[72,1969,1970,1975],{},[40,1971,1972,1974],{},[15,1973,1470],{}," 한 번 돌려보기"," — 라이브에서 schema 자동 생성해 우리 수기 정의와 diff를 보면 다른 drift도 발견 가능. Aurora 직결 경로 운영 절차 정착 후 진행.",[856,1977],{},[10,1979,1981,1982,897,1984,897,1987,1989],{"id":1980},"_4-사용자단-인증-백엔드-연동-authsignupauthloginme-실-api-연동-배포-49","§4. 사용자단 인증 백엔드 연동 — ",[15,1983,1067],{},[15,1985,1986],{},"\u002Fauth\u002Flogin",[15,1988,1050],{}," 실 API 연동 (배포 #49)",[24,1991,868],{"id":1992},"한-줄-2",[29,1994,1995,1996,1999,2000,2003,2004,2007,2008,2011,2012,2014,2015,2018,2019,2022,2023,2026,2027,2030,2031,2033,2034,2037,2038,897,2041,2044,2045,63],{},"사용자단의 모든 화면이 목업 데이터로 동작하던 상태에서 ",[40,1997,1998],{},"인증·계정 영역을 첫 번째로 실 API에 연결",". JWT를 ",[15,2001,2002],{},"auth-token"," 쿠키에 저장, ",[15,2005,2006],{},"useApi()"," $fetch 래퍼가 자동으로 ",[15,2009,2010],{},"Authorization: Bearer"," 주입, 글로벌 미들웨어는 쿠키 존재만으로 1차 가드, 클라이언트 부트스트랩 플러그인이 ",[15,2013,1050],{},"로 스토어 풀 컨텍스트 페치. 회원가입은 Step 4(본인 인증) → Step 5(완료) 전이에서 실 API 호출 + 토큰 저장 + 자동 로그인 → ",[15,2016,2017],{},"\u002Fhome"," 이동, 로그인은 ",[15,2020,2021],{},"companyId"," 쿠키(",[15,2024,2025],{},"last-company-id",", 1년) 자동 사용 + 없으면 필드 노출. 5 파일 수정 + 1 파일 신규(",[15,2028,2029],{},"plugins\u002Fauth.client.ts","), typecheck 통과, 로컬 + 프로덕션 모두 e2e 검증(쿠키 동봉 시 ",[15,2032,2017],{}," 200, 없으면 ",[15,2035,2036],{},"\u002Flogin?redirect=\u002Fhome"," 리다이렉트, ",[15,2039,2040],{},"\u002Flogin",[15,2042,2043],{},"\u002Fsignup"," 200). Cloudflare Pages 배포 #49 (alias ",[15,2046,2047],{},"9be4ff61.malgn-noti.pages.dev",[24,2049,2051],{"id":2050},"_41-api-계약-사전-확인-변경-없음","4.1 API 계약 사전 확인 (변경 없음)",[29,2053,2054,2057,2058,2060],{},[15,2055,2056],{},"malgn-noti-api\u002Fsrc\u002Froutes\u002Fauth.ts"," (배포 #8 ",[15,2059,1445],{}," 그대로):",[910,2062,2063,2076],{},[913,2064,2065],{},[916,2066,2067,2070,2073],{},[919,2068,2069],{},"라우트",[919,2071,2072],{},"요청",[919,2074,2075],{},"응답",[932,2077,2078,2096,2113],{},[916,2079,2080,2085,2091],{},[937,2081,2082],{},[15,2083,2084],{},"POST \u002Fauth\u002Fsignup",[937,2086,2087,2090],{},[15,2088,2089],{},"{ companyName, loginid, password, name?, email?, phone? }"," (Zod)",[937,2092,2093],{},[15,2094,2095],{},"201 { data: { user: {id, loginid, name, role}, company: {id, name}, token } }",[916,2097,2098,2103,2108],{},[937,2099,2100],{},[15,2101,2102],{},"POST \u002Fauth\u002Flogin",[937,2104,2105,2090],{},[15,2106,2107],{},"{ companyId: number, loginid, password }",[937,2109,2110],{},[15,2111,2112],{},"200 { data: { user, company:{id}, token } }",[916,2114,2115,2121,2123],{},[937,2116,2117,2120],{},[15,2118,2119],{},"GET \u002Fme"," (Bearer)",[937,2122,980],{},[937,2124,2125],{},[15,2126,2127],{},"200 { data: { user, company, ctxRole } }",[29,2129,2130,77,2133,2136,2137,2139,2140,2142],{},[40,2131,2132],{},"핵심 제약",[15,2134,2135],{},"loginid","는 회사 스코프 내 unique (composite UNIQUE on company_id, loginid) → ",[15,2138,1986],{},"이 ",[15,2141,2021],{},"를 필수로 받음. 사용자가 자신의 companyId를 항상 알기 어려우므로 UX 보정 필요.",[24,2144,2146],{"id":2145},"_42-결정-사항","4.2 결정 사항",[69,2148,2149,2157,2167,2188],{},[72,2150,2151,77,2154,2156],{},[40,2152,2153],{},"JWT 저장 위치",[15,2155,2002],{}," 쿠키 (maxAge 7일, sameSite=lax, secure는 PROD에서만). 백엔드가 Set-Cookie를 안 쓰고 응답 본문에 토큰을 담아 보내므로 일반 쿠키(HttpOnly 아님). 향후 백엔드가 Set-Cookie + HttpOnly + SameSite=Strict로 응답하면 그쪽으로 이관.",[72,2158,2159,77,2164,2166],{},[40,2160,2161,2163],{},[15,2162,2021],{}," 자동 사용",[15,2165,2025],{}," 쿠키(1년)에 회원가입\u002F로그인 시 저장 → 다음 로그인 폼에서 자동 사용. 새 브라우저\u002F쿠키 삭제 시에만 \"고객사 ID\" 필드 노출.",[72,2168,2169,2172,2173,2175,2176,2179,2180,2183,2184,2187],{},[40,2170,2171],{},"SSR 안전성",": 미들웨어는 쿠키 존재만 확인 → 통과\u002F리다이렉트만 결정. ",[15,2174,1050],{}," 호출(스토어 페치)은 ",[40,2177,2178],{},"클라이언트 플러그인","으로 분리 — Pinia store action 안에서 ",[15,2181,2182],{},"useCookie()"," 호출이 SSR 미들웨어 컨텍스트를 잃어 ",[15,2185,2186],{},"\"composable was called outside of …\""," 에러를 내는 문제 회피(실제 발생 → 수정 후 통과).",[72,2189,2190,2193,2194,2196],{},[40,2191,2192],{},"회원가입 흐름",": Step 4(본인 인증 완료) → \"가입 완료\" 클릭 → 실 API 호출 → 성공 시 Step 5(완료) 노출 + 자동 로그인 + 발급 고객사 ID 표시 → \"대시보드로 이동\" 클릭 시 ",[15,2195,2017],{},". 실패 시 토스트 + 단계 유지.",[24,2198,2200],{"id":2199},"_43-코드-변경-6-파일","4.3 코드 변경 (6 파일)",[910,2202,2203,2213],{},[913,2204,2205],{},[916,2206,2207,2210],{},[919,2208,2209],{},"파일",[919,2211,2212],{},"변경",[932,2214,2215,2241,2288,2306,2321,2357,2393],{},[916,2216,2217,2222],{},[937,2218,2219],{},[15,2220,2221],{},"app\u002Fcomposables\u002FuseApi.ts",[937,2223,2224,897,2227,2230,2231,2234,2235,2237,2238,2240],{},[15,2225,2226],{},"useAuthToken()",[15,2228,2229],{},"useLastCompanyId()"," 쿠키 헬퍼 export. $fetch ",[15,2232,2233],{},"onRequest","에서 토큰 자동 ",[15,2236,2010],{}," 주입. 401 응답 시 토큰 클리어 + 스토어 클리어 + ",[15,2239,2040],{}," 이동.",[916,2242,2243,2248],{},[937,2244,2245],{},[15,2246,2247],{},"app\u002Fstores\u002Fauth.ts",[937,2249,2250,897,2253,2256,2257,2259,2260,1608,2263,1608,2266,1608,2269,2272,2273,2276,2277,2280,2281,2283,2284,2287],{},[15,2251,2252],{},"AuthUser",[15,2254,2255],{},"AuthCompany"," 타입 신규 (",[15,2258,2056],{}," 응답 형상 그대로). ",[15,2261,2262],{},"signup()",[15,2264,2265],{},"login()",[15,2267,2268],{},"fetchMe()",[15,2270,2271],{},"logout()"," 액션. ",[15,2274,2275],{},"signup","은 응답을 즉시 store에 hydrate해 isAuthed=true. ",[15,2278,2279],{},"login","은 응답 hydrate + ",[15,2282,1050],{},"로 풀 컨텍스트 보강. ",[15,2285,2286],{},"fetchMe","는 토큰 만료 시 토큰 클리어 후 false 반환.",[916,2289,2290,2295],{},[937,2291,2292],{},[15,2293,2294],{},"app\u002Fmiddleware\u002Fauth.global.ts",[937,2296,2297,2298,2301,2302,2305],{},"토큰 쿠키 존재 여부만 확인 (SSR 안전). 없으면 ",[15,2299,2300],{},"\u002Flogin?redirect=…"," 리다이렉트. ",[15,2303,2304],{},"meta.auth === false","는 그대로 통과.",[916,2307,2308,2314],{},[937,2309,2310,2313],{},[15,2311,2312],{},"app\u002Fplugins\u002Fauth.client.ts"," (신규)",[937,2315,2316,2317,2320],{},"클라이언트 부트스트랩 1회 — 토큰 쿠키가 있고 store가 비어 있으면 ",[15,2318,2319],{},"auth.fetchMe()"," 호출. SSR 미들웨어 컨텍스트 손실 문제를 피해 클라이언트 측에서 처리.",[916,2322,2323,2328],{},[937,2324,2325],{},[15,2326,2327],{},"app\u002Fpages\u002Flogin\u002Findex.vue",[937,2329,2330,897,2333,2335,2336,2338,2339,2342,2343,2139,2346,2349,2350,2353,2354,2356],{},[15,2331,2332],{},"useAuthStore()",[15,2334,2229],{}," 사용. ",[15,2337,2025],{}," 쿠키가 있으면 자동 사용, 없으면 \"고객사 ID\" 필드(",[15,2340,2341],{},"v-if=\"needCompanyId\"",") 노출. ",[15,2344,2345],{},"onLogin()",[15,2347,2348],{},"auth.login()"," 호출 → 성공 시 ",[15,2351,2352],{},"redirect"," 쿼리(",[15,2355,2017],{}," 기본)로 이동. 401 응답은 \"아이디 또는 비밀번호가 올바르지 않습니다\" 토스트.",[916,2358,2359,2364],{},[937,2360,2361],{},[15,2362,2363],{},"app\u002Fpages\u002Fsignup.vue",[937,2365,2366,2369,2370,2373,2374,2377,2378,2380,2381,2349,2384,2387,2388,1107,2391,262],{},[15,2367,2368],{},"goNext()","가 ",[15,2371,2372],{},"step.value === 4","일 때 ",[15,2375,2376],{},"submitSignup()"," 호출(이전: 단순 step 증가만). ",[15,2379,2376],{},"은 ",[15,2382,2383],{},"auth.signup({companyName, loginid: email, password, email, name, phone})",[15,2385,2386],{},"step.value = 5","로 완료 화면 노출, 실패 시 409 응답을 \"이미 가입된 이메일입니다\" 안내. Step 5는 발급 고객사 ID 표시 + \"대시보드로 이동\" 버튼이 ",[15,2389,2390],{},"finish()",[15,2392,2017],{},[916,2394,2395,2400],{},[937,2396,2397],{},[15,2398,2399],{},"nuxt.config.ts",[937,2401,2402,2405,2406,1107,2409,2412,2413,2416],{},[15,2403,2404],{},"runtimeConfig.public.apiBaseUrl"," 기본값을 ",[15,2407,2408],{},"'\u002Fapi'",[15,2410,2411],{},"'https:\u002F\u002Fmalgn-noti-api.malgnsoft.workers.dev'","로 변경. ",[15,2414,2415],{},"NUXT_PUBLIC_API_BASE_URL","로 그대로 override 가능.",[24,2418,2420],{"id":2419},"_44-발견된-ssr-이슈-우회-의미-있는-발견","4.4 발견된 SSR 이슈 + 우회 (의미 있는 발견)",[29,2422,2423,2424,2427,2428,2431],{},"첫 시도(",[15,2425,2426],{},"auth.global.ts","에서 ",[15,2429,2430],{},"await auth.fetchMe()"," 호출)는 500 에러:",[347,2433,2438],{"className":2434,"code":2436,"language":2437},[2435],"language-text","[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin,\n       Nuxt hook, Nuxt middleware, or Vue setup function.\n  at useCookie (...cookie.js:38:19)\n  at useAuthToken (...\u002FuseApi.ts:15:45)\n  at Proxy.fetchMe (...\u002Fauth.ts:70:47)\n  at ...\u002Fauth.global.ts:15:104\n","text",[15,2439,2436],{"__ignoreMap":352},[29,2441,2442,2443,2445,2446,2448,2449,2451],{},"원인 — Pinia store action(",[15,2444,2286],{},") 내부에서 ",[15,2447,2182],{},"(via ",[15,2450,2226],{},")를 호출하면, await 경계를 넘으면서 Nuxt instance 컨텍스트가 끊김. 동기 미들웨어 함수 안에서는 통하지만 store action 안에서는 컨텍스트 보장이 약함.",[29,2453,2454,2455,2457,2458,2460],{},"해결 — SSR 미들웨어는 쿠키 존재만 확인하고 통과 결정. ",[15,2456,1050],{}," 검증은 클라이언트 부트스트랩 플러그인에서 1회만 호출. 토큰이 위조\u002F만료면 fetchMe가 토큰을 클리어하고 false 반환 → 다음 라우트 가드에서 ",[15,2459,2040],{},"으로 리다이렉트. SSR 비용 0 + 안전.",[24,2462,2464],{"id":2463},"_45-검증-로컬-프로덕션","4.5 검증 (로컬 + 프로덕션)",[29,2466,2467],{},"API 통한 e2e 4건 × 2환경(로컬 dev + 프로덕션 Pages):",[910,2469,2470,2481],{},[913,2471,2472],{},[916,2473,2474,2476,2479],{},[919,2475,1223],{},[919,2477,2478],{},"기대",[919,2480,930],{},[932,2482,2483,2499,2511,2528,2541],{},[916,2484,2485,2491,2496],{},[937,2486,2487,2490],{},[15,2488,2489],{},"GET \u002Fhome"," (토큰 쿠키 없음)",[937,2492,2493,2494],{},"302 → ",[15,2495,2036],{},[937,2497,2498],{},"✅",[916,2500,2501,2506,2509],{},[937,2502,2503,2505],{},[15,2504,2489],{}," (토큰 쿠키 동봉)",[937,2507,2508],{},"200 (통과)",[937,2510,2498],{},[916,2512,2513,2523,2526],{},[937,2514,2515,2518,2519,2522],{},[15,2516,2517],{},"GET \u002Flogin"," (",[15,2520,2521],{},"meta.auth: false",")",[937,2524,2525],{},"200",[937,2527,2498],{},[916,2529,2530,2537,2539],{},[937,2531,2532,2518,2535,2522],{},[15,2533,2534],{},"GET \u002Fsignup",[15,2536,2521],{},[937,2538,2525],{},[937,2540,2498],{},[916,2542,2543,2550,2552],{},[937,2544,2545,2518,2547,2522],{},[15,2546,850],{},[15,2548,2549],{},"auth: false",[937,2551,2525],{},[937,2553,2554],{},"✅ (회귀 없음)",[29,2556,2557,2558,2560,2561,2563],{},"토큰 자체는 ",[15,2559,1067],{}," 라이브 호출로 발급 → 쿠키 동봉으로 SSR 진입 → 미들웨어 통과 확인. 클라이언트 측 ",[15,2562,1050],{}," 호출은 브라우저 환경 필요라 별도 수동 점검 필요(다음 단계).",[232,2565,2567],{"id":2566},"_451-발견된-ssr-컨텍스트-버그첫-미들웨어-버전-우회-후-통과-확인","4.5.1 발견된 SSR 컨텍스트 버그(첫 미들웨어 버전) — 우회 후 통과 확인",[69,2569,2570,2578],{},[72,2571,2572,2573,2427,2575,2577],{},"1차 시도: ",[15,2574,2426],{},[15,2576,2430],{}," 직접 호출 → 500 (위 §4.4).",[72,2579,2580,2581,2583],{},"2차 시도: 미들웨어 단순화 + ",[15,2582,2029],{}," 신설 → 500 → 200 회복 확인.",[24,2585,2587],{"id":2586},"_46-배포-49","4.6 배포 #49",[69,2589,2590,2597,2602],{},[72,2591,2592,705,2594,2596],{},[15,2593,704],{},[15,2595,708],{}," 프리셋. login\u002Fsignup 청크 + auth plugin 청크 새로 생성.",[72,2598,2599,262],{},[15,2600,2601],{},"npx wrangler@4 pages deploy dist --project-name=malgn-noti --branch=main --commit-dirty=true --commit-message \"user-side auth integration\"",[72,2603,729,2604,2607,2608,2611],{},[15,2605,2606],{},"https:\u002F\u002F9be4ff61.malgn-noti.pages.dev",". 프로덕션 ",[15,2609,2610],{},"https:\u002F\u002Fmalgn-noti.pages.dev"," 갱신.",[24,2613,2615],{"id":2614},"_47-산출물","4.7 산출물",[69,2617,2618,2624,2645,2651,2654],{},[72,2619,2620,2623],{},[15,2621,2622],{},"malgn-noti: 사용자단 인증 백엔드 연동 (배포 #49)"," — 6 파일 수정 + 1 신규.",[72,2625,2626,2627,1608,2630,1608,2633,1608,2636,1608,2639,1608,2642,262],{},"수정: ",[79,2628,2221],{"href":2629},"..\u002F..\u002Fapp\u002Fcomposables\u002FuseApi.ts",[79,2631,2247],{"href":2632},"..\u002F..\u002Fapp\u002Fstores\u002Fauth.ts",[79,2634,2294],{"href":2635},"..\u002F..\u002Fapp\u002Fmiddleware\u002Fauth.global.ts",[79,2637,2327],{"href":2638},"..\u002F..\u002Fapp\u002Fpages\u002Flogin\u002Findex.vue",[79,2640,2363],{"href":2641},"..\u002F..\u002Fapp\u002Fpages\u002Fsignup.vue",[79,2643,2399],{"href":2644},"..\u002F..\u002Fnuxt.config.ts",[72,2646,2647,2648,262],{},"신규: ",[79,2649,2312],{"href":2650},"..\u002F..\u002Fapp\u002Fplugins\u002Fauth.client.ts",[72,2652,2653],{},"WBS 갱신: 5-3-15 ⚪ → 🟢 (인증·계정 실 API 연동 완료, 발송·이력 등 나머지 점진 교체).",[72,2655,2656],{},"라이브 API\u002FDB — 변경 없음(쓰기는 검증 과정의 임시 계정 4건, 모두 cleanup).",[24,2658,2660],{"id":2659},"_48-알려진-한계-다음-단계","4.8 알려진 한계 \u002F 다음 단계",[69,2662,2663,2678,2686,2692,2718,2730],{},[72,2664,2665,2673,2674,2677],{},[40,2666,2667,2669,2670,2672],{},[15,2668,1986],{},"의 ",[15,2671,2021],{}," 요구"," — 새 브라우저에서 사용자가 자신의 ID를 외워야 함. 후속에서 ",[15,2675,2676],{},"\u002Fauth\u002Flogin-by-email"," 등 이메일 → 회사 lookup 라우트 추가 고려.",[72,2679,2680,2685],{},[40,2681,2682,2684],{},[15,2683,1050],{}," SSR 검증 분리"," — 토큰이 유효한지 라우트 진입 시점에 서버에서 확인하지 않으므로, 만료된 토큰이라도 1회는 페이지가 로드되고 그 뒤 클라이언트 fetchMe에서 401 처리. 보안상 큰 문제는 아니지만 첫 페인트 후 리다이렉트가 깜빡일 수 있음.",[72,2687,2688,2691],{},[40,2689,2690],{},"HttpOnly 미적용"," — 토큰이 JS에서 읽힘. XSS 발생 시 토큰 탈취 가능. 백엔드가 Set-Cookie + HttpOnly로 응답하도록 확장 시 클라이언트 코드 단순화 + 보안 강화.",[72,2693,2694,2704,2705,410,2708,410,2711,410,2714,2717],{},[40,2695,2696,2697,2700,2701,2703],{},"OTP 인증(",[15,2698,2699],{},"TB_VERIFICATION",")·약관 동의(",[15,2702,1342],{},")·서비스 담당자 초대"," — signup 라우트는 이를 적재하지 않음. 프런트에서 입력은 받지만 백엔드는 무시. 후속 라우트(",[15,2706,2707],{},"\u002Fauth\u002Fverify-email",[15,2709,2710],{},"\u002Fauth\u002Fverify-phone",[15,2712,2713],{},"\u002Fauth\u002Fagree-terms",[15,2715,2716],{},"\u002Fmanager-invites",") 구현 필요.",[72,2719,2720,2723,2724,410,2726,2729],{},[40,2721,2722],{},"나머지 화면 연동"," — 발송 6채널·이력·주소록·발신정보·템플릿·캠페인·크레딧·문의·나의 페이지 — 모두 여전히 목업. 화면별 도메인 API(",[15,2725,1053],{},[15,2727,2728],{},"\u002Fsender-phones"," 등)로 점진 교체.",[72,2731,2732,2735,2736,2739,2740,2743],{},[40,2733,2734],{},"로그아웃 UX"," — 현재 ",[15,2737,2738],{},"useAuthStore().logout()","만 정의. GNB의 로그아웃 버튼은 ",[15,2741,2742],{},"AppGnb.vue","에서 데모용 ref만 토글 중 — 실 호출로 교체 필요.",[856,2745],{},[10,2747,2749,2750,897,2753,2756],{"id":2748},"_5-이메일-otp-인증-authemail-codesendverify-신설-signupvue-실-api-연동-배포-950","§5. 이메일 OTP 인증 — ",[15,2751,2752],{},"\u002Fauth\u002Femail-code\u002Fsend",[15,2754,2755],{},"\u002Fverify"," 신설 + signup.vue 실 API 연동 (배포 #9·#50)",[24,2758,868],{"id":2759},"한-줄-3",[29,2761,2762,2763,847,2765,2768,2769,2772,2773,2669,2776,1475,2779,2782,2783,1475,2786,2789,2790,2793,2794,2797],{},"§4의 알려진 한계 #4(이메일 OTP 미연동, 화면용 토스트만 동작)를 해소. 백엔드에 OTP 발송·검증 라우트 2개 추가(TB_VERIFICATION 적재 + SHA-256 코드 해시 + TTL 10분·재발송 시 직전 코드 만료·5회 시도 제한·소비 후 재사용 차단), Drizzle ",[15,2764,1526],{},[15,2766,2767],{},"verification"," 정의(라이브 정본과 인덱스 일치), OpenAPI 4지점 갱신(2 paths + 3 schemas), Workers 배포 #9(Version ",[15,2770,2771],{},"83f32a61...","). 프런트 ",[79,2774,2775],{"href":2641},"signup.vue",[15,2777,2778],{},"sendIdCode",[15,2780,2781],{},"confirmIdCode","를 실 API 호출로 교체 + 버튼 로딩 상태(",[15,2784,2785],{},"sendingCode",[15,2787,2788],{},"verifyingCode",") + 재발송 라벨 + 에러 메시지 표준화. NHN_MOCK=1 환경에서만 응답에 ",[15,2791,2792],{},"mockCode"," 노출(개발 편의 — production 자동 차단). Pages 배포 #50 (alias ",[15,2795,2796],{},"c2100890.malgn-noti.pages.dev","). 라이브 e2e 6 시나리오 모두 통과 (발송·잘못된 코드 401·올바른 코드 200·소비 후 재시도 401·재발송 신규 코드·DB 행 검증).",[24,2799,2801],{"id":2800},"_51-결정-사항","5.1 결정 사항",[69,2803,2804,2820,2829,2843,2852,2862,2876,2891],{},[72,2805,2806,77,2809,2811,2812,2815,2816,2819],{},[40,2807,2808],{},"저장",[15,2810,2699],{}," (이미 라이브) 활용. ",[15,2813,2814],{},"code_hash","로 평문 코드 저장 회피. 해시는 ",[15,2817,2818],{},"SHA-256(target|purpose|code)"," — Web Crypto API 네이티브.",[72,2821,2822,2825,2826,262],{},[40,2823,2824],{},"TTL",": 10분. ",[15,2827,2828],{},"expires_at = now + 10*60*1000",[72,2830,2831,2834,2835,2838,2839,2842],{},[40,2832,2833],{},"재발송",": 같은 ",[15,2836,2837],{},"(email, purpose)","의 미소비·미만료 레코드를 즉시 만료 처리(",[15,2840,2841],{},"expires_at = now",") → 직전 코드로 검증 불가.",[72,2844,2845,2848,2849,63],{},[40,2846,2847],{},"시도 제한",": 5회 초과 시 즉시 만료(",[15,2850,2851],{},"OTP_MAX_ATTEMPTS = 5",[72,2853,2854,2857,2858,2861],{},[40,2855,2856],{},"소비",": 검증 성공 시 ",[15,2859,2860],{},"consumed_at = now"," → 같은 코드 재사용 차단.",[72,2863,2864,77,2867,1259,2869,1259,2872,2875],{},[40,2865,2866],{},"purpose enum",[15,2868,2275],{},[15,2870,2871],{},"reset_password",[15,2873,2874],{},"change_email",". signup 외 흐름은 후속 라우트가 활용.",[72,2877,2878,77,2883,2886,2887,2890],{},[40,2879,2880,2882],{},[15,2881,2792],{}," 노출",[15,2884,2885],{},"c.env.NHN_MOCK === '1'","일 때만 응답에 ",[15,2888,2889],{},"mockCode: code"," 포함. production은 secret 미설정이면 자동으로 노출 안 됨. real NHN 자격증명 등록 후 secret도 영구 제거.",[72,2892,2893,2898],{},[40,2894,2895,2897],{},[15,2896,1067],{},"에 강제 검증 미추가",": 후속 결정 사항(검증 게이트 도입 시점)이 필요하므로 본 단계에서는 백엔드 호환성 유지 + 프런트가 UX 차원에서 검증 강제. 이전 e2e 테스트·기존 통합 사용처에 영향 없음.",[24,2900,2902,2903,2522],{"id":2901},"_52-코드-변경-백엔드-malgn-noti-api","5.2 코드 변경 (백엔드 — ",[15,2904,846],{},[910,2906,2907,2915],{},[913,2908,2909],{},[916,2910,2911,2913],{},[919,2912,2209],{},[919,2914,2212],{},[932,2916,2917,2933,2951,2988,3013],{},[916,2918,2919,2924],{},[937,2920,2921],{},[79,2922,1466],{"href":2923},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Fdb\u002Fschema.ts",[937,2925,2926,2928,2929,2932],{},[15,2927,2767],{}," 테이블 신규 정의 (8 컬럼 + ",[15,2930,2931],{},"idx_verif_target(target_type, target, purpose, expires_at)"," — 라이브 정본과 1:1).",[916,2934,2935,2941],{},[937,2936,2937],{},[79,2938,2940],{"href":2939},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Flib\u002Ferrors.ts","src\u002Flib\u002Ferrors.ts",[937,2942,2943,2946,2947,2950],{},[15,2944,2945],{},"errors.unauthenticated()","에 default 메시지 파라미터 추가 (",[15,2948,2949],{},"(msg = 'Authentication required')","). 호환성 유지 + OTP 라우트에서 한국어 메시지 첨부 가능.",[916,2952,2953,2959],{},[937,2954,2955],{},[79,2956,2958],{"href":2957},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Froutes\u002Fauth.ts","src\u002Froutes\u002Fauth.ts",[937,2960,2961,2962,2965,2966,2969,2970,410,2973,2976,2977,108,2980,2983,2984,2987],{},"헬퍼 4개 추가: ",[15,2963,2964],{},"generateOtpCode()"," (Web Crypto getRandomValues 4 bytes → 6 digits), ",[15,2967,2968],{},"hashOtpCode()"," (SHA-256), ",[15,2971,2972],{},"purposeLabel()",[15,2974,2975],{},"buildEmailBody()"," (HTML 템플릿). 라우트 2개: ",[15,2978,2979],{},"POST \u002Fauth\u002Femail-code\u002Fsend",[15,2981,2982],{},"POST \u002Fauth\u002Femail-code\u002Fverify",". NHN Email 어댑터 호출(",[15,2985,2986],{},"sendEmail(null, ...)",") — 자격증명 미설정 시 어댑터 내부에서 mock fallback.",[916,2989,2990,2996],{},[937,2991,2992],{},[79,2993,2995],{"href":2994},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Fopenapi.ts","src\u002Fopenapi.ts",[937,2997,2998,2999,897,3001,3003,3004,897,3007,897,3010,63],{},"2 paths(",[15,3000,2752],{},[15,3002,2755],{},") + 3 schemas(",[15,3005,3006],{},"EmailCodeSendRequest",[15,3008,3009],{},"EmailCodeSendResponse",[15,3011,3012],{},"EmailCodeVerifyRequest",[916,3014,3015,3020],{},[937,3016,3017],{},[15,3018,3019],{},"wrangler.toml",[937,3021,3022],{},"변경 없음.",[24,3024,3026,3027,2522],{"id":3025},"_53-코드-변경-프런트-malgn-noti","5.3 코드 변경 (프런트 — ",[15,3028,154],{},[910,3030,3031,3039],{},[913,3032,3033],{},[916,3034,3035,3037],{},[919,3036,2209],{},[919,3038,2212],{},[932,3040,3041],{},[916,3042,3043,3047],{},[937,3044,3045],{},[79,3046,2363],{"href":2641},[937,3048,3049,1107,3052,3054,3055,3057,3058,3060,3061,1259,3064,1259,3066,3069,3070,1107,3073,3075,3076,897,3079,897,3082,3085,3086,3088,3089,262],{},[15,3050,3051],{},"sendIdCode()",[15,3053,2979],{}," async. 응답 ",[15,3056,2792],{}," 있으면 토스트에 노출(개발 편의). ",[15,3059,2785],{}," 로딩 ref. 버튼 라벨 ",[15,3062,3063],{},"발송 중…",[15,3065,2833],{},[15,3067,3068],{},"인증코드 발송"," 3-상태. ",[15,3071,3072],{},"confirmIdCode()",[15,3074,2982],{}," async. 백엔드 한국어 에러 메시지(",[15,3077,3078],{},"인증코드가 만료되었거나 …",[15,3080,3081],{},"시도 횟수를 초과했습니다 …",[15,3083,3084],{},"인증코드가 올바르지 않습니다",")를 그대로 토스트에. ",[15,3087,2788],{}," 로딩 + 버튼 라벨 ",[15,3090,3091],{},"확인 중…",[24,3093,3095],{"id":3094},"_54-라이브-e2e-검증-production","5.4 라이브 e2e 검증 (Production)",[29,3097,3098,3101],{},[15,3099,3100],{},"NHN_MOCK=1"," secret을 production에 일시 적용 → 6 시나리오 검증 → secret 즉시 제거.",[910,3103,3104,3116],{},[913,3105,3106],{},[916,3107,3108,3111,3114],{},[919,3109,3110],{},"#",[919,3112,3113],{},"시나리오",[919,3115,930],{},[932,3117,3118,3136,3156,3171,3185,3203],{},[916,3119,3120,3123,3134],{},[937,3121,3122],{},"1",[937,3124,3125,3127,3128,108,3131],{},[15,3126,2979],{}," → 200 + ",[15,3129,3130],{},"mockCode: \"092004\"",[15,3132,3133],{},"expiresAt",[937,3135,2498],{},[916,3137,3138,3141,3154],{},[937,3139,3140],{},"2",[937,3142,3143,3146,3147,3150,3151],{},[15,3144,3145],{},"POST \u002Fverify"," 잘못된 코드(",[15,3148,3149],{},"\"000000\"",") → 401 ",[15,3152,3153],{},"인증코드가 올바르지 않습니다.",[937,3155,2498],{},[916,3157,3158,3161,3169],{},[937,3159,3160],{},"3",[937,3162,3163,3165,3166],{},[15,3164,3145],{}," 올바른 코드 → 200 + ",[15,3167,3168],{},"{verified:true}",[937,3170,2498],{},[916,3172,3173,3176,3183],{},[937,3174,3175],{},"4",[937,3177,3178,3179,3182],{},"같은 코드 재시도 → 401 ",[15,3180,3181],{},"인증코드가 만료되었거나 발급된 적이 없습니다."," (consumed)",[937,3184,2498],{},[916,3186,3187,3190,3201],{},[937,3188,3189],{},"5",[937,3191,3192,3193,3196,3197,3200],{},"재발송 → 새 코드(",[15,3194,3195],{},"461872",") + 직전 코드(",[15,3198,3199],{},"092004",") 즉시 만료",[937,3202,2498],{},[916,3204,3205,3208,3211],{},[937,3206,3207],{},"6",[937,3209,3210],{},"DB 행 점검 — id=1 attempts=1+consumed_at, id=2 신규+expires_at 신규",[937,3212,2498],{},[29,3214,3215,3216,3219,3220,1107,3223,3226,3227,3229],{},"검증 후 ",[15,3217,3218],{},"NHN_MOCK"," secret ",[15,3221,3222],{},"wrangler secret delete NHN_MOCK",[15,3224,3225],{},"wrangler secret list"," 응답에 JWT_SECRET만 잔존 확인. 재호출 시 응답에서 ",[15,3228,2792],{}," 사라짐 확인.",[29,3231,3232],{},"검증 과정에서 생성된 TB_VERIFICATION 임시 행은 즉시 cleanup.",[24,3234,3236],{"id":3235},"_55-배포-950","5.5 배포 #9·#50",[69,3238,3239,3254],{},[72,3240,3241,77,3244,3246,3247,3250,3251,262],{},[40,3242,3243],{},"API (Workers)",[15,3245,1924],{}," 통과 → ",[15,3248,3249],{},"pnpm run deploy"," → Version ",[15,3252,3253],{},"83f32a61-ca2c-4094-ae21-0cfcb174f26c",[72,3255,3256,77,3259,1107,3261,3264,3265,262],{},[40,3257,3258],{},"사용자단 (Pages)",[15,3260,704],{},[15,3262,3263],{},"npx wrangler@4 pages deploy dist --project-name=malgn-noti --branch=main --commit-dirty=true --commit-message \"email OTP integration\""," → alias ",[15,3266,3267],{},"https:\u002F\u002Fc2100890.malgn-noti.pages.dev",[24,3269,3271],{"id":3270},"_56-산출물","5.6 산출물",[69,3273,3274,3277,3280,3283,3286],{},[72,3275,3276],{},"API: 4 파일 수정 — schema.ts + errors.ts + auth.ts + openapi.ts.",[72,3278,3279],{},"사용자단: 1 파일 수정 — signup.vue.",[72,3281,3282],{},"SIGNUP.md §8 #4 → ✅ (이메일) + #4b 휴대폰 미연동으로 재구성.",[72,3284,3285],{},"라이브 DB — TB_VERIFICATION에 라이브 행이 실 사용자 가입 시점부터 적재 시작 (검증용 임시 행은 cleanup).",[72,3287,3288,3289,3291],{},"라이브 Worker — 변경 #9 적용. ",[15,3290,1001],{}," 정상.",[24,3293,3295],{"id":3294},"_57-알려진-한계-다음-단계","5.7 알려진 한계 \u002F 다음 단계",[69,3297,3298,3315,3327,3339,3347],{},[72,3299,3300,3303,3304,2139,3307,3310,3311,3314],{},[40,3301,3302],{},"실제 이메일은 발송되지 않음"," — TB_NHN_CREDENTIAL 비어 있어 ",[15,3305,3306],{},"sendEmail",[15,3308,3309],{},"(creds=null, mockMode=false)"," → 어댑터 내부 mock fallback. 사용자가 가입 시 코드 자체는 발급되지만 메일함에 도착 0. ",[40,3312,3313],{},"자격증명 등록은 별도 작업","(NHN Cloud 콘솔에서 채널별 appKey 발급 → TB_NHN_CREDENTIAL 적재 + envelope 암호화). 그 전까지는 NHN_MOCK secret을 운영자가 일시 적용해 mockCode 확인 가능.",[72,3316,3317,3320,3321,897,3324,3326],{},[40,3318,3319],{},"휴대폰 OTP 미연동"," — Step 4(휴대폰 본인 인증)는 여전히 화면 더미. 인증 사업자(PASS·NICE 등) 선정 후 어댑터 + ",[15,3322,3323],{},"\u002Fauth\u002Fphone-code\u002Fsend",[15,3325,2755],{}," 동일 패턴 신설.",[72,3328,3329,3334,3335,3338],{},[40,3330,3331,3333],{},[15,3332,1067],{}," 강제 검증 미적용"," — 정책 결정 후 적용 가능(",[15,3336,3337],{},"emailVerificationToken"," 필수화 또는 signup 직전 TB_VERIFICATION 조회).",[72,3340,3341,3346],{},[40,3342,3343,3345],{},[15,3344,1526],{}," 인덱스 누락 점검"," — 본 단계에서 verification만 인덱스\u002FFK 명시화. 다른 테이블은 §3에서 export\u002Fflow 4 테이블만 처리한 상태 그대로.",[72,3348,3349,3352],{},[40,3350,3351],{},"Rate limit"," — IP·이메일별 분 단위 발송 제한 미적용. 후속 작업으로 Cloudflare KV 또는 Durable Objects 카운터 도입 검토.",[856,3354],{},[3356,3357,3358],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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);}",{"title":352,"searchDepth":465,"depth":465,"links":3360},[3361,3362,3363,3364,3371,3372,3373,3374,3375,3376,3377,3378,3379,3380,3381,3383,3384,3385,3386,3387,3388,3389,3390,3391,3392,3393,3394,3395,3396,3397,3400,3401,3402,3403,3404,3405,3407,3409,3410,3411,3412],{"id":26,"depth":393,"text":27},{"id":66,"depth":393,"text":67},{"id":142,"depth":393,"text":143},{"id":229,"depth":393,"text":230,"children":3365},[3366,3368,3370],{"id":234,"depth":465,"text":3367},"3.1 doc\u002FWBS.md — 텍스트 정본 (신규)",{"id":314,"depth":465,"text":3369},"3.2 app\u002Fpages\u002Fwbs.vue — 공개 라이브 카탈로그 (신규)",{"id":640,"depth":465,"text":641},{"id":696,"depth":393,"text":697},{"id":756,"depth":393,"text":756},{"id":790,"depth":393,"text":791},{"id":867,"depth":393,"text":868},{"id":907,"depth":393,"text":908},{"id":1012,"depth":393,"text":1013},{"id":1081,"depth":393,"text":1082},{"id":1128,"depth":393,"text":1129},{"id":1213,"depth":393,"text":1214},{"id":1307,"depth":393,"text":1308},{"id":1375,"depth":393,"text":3382},"2.7 0002_export_flow.sql 라이브 정본 동기화",{"id":1427,"depth":393,"text":1428},{"id":1455,"depth":393,"text":1456},{"id":1530,"depth":393,"text":868},{"id":1560,"depth":393,"text":1561},{"id":1692,"depth":393,"text":1693},{"id":1853,"depth":393,"text":1854},{"id":1895,"depth":393,"text":1896},{"id":1943,"depth":393,"text":1944},{"id":1958,"depth":393,"text":1959},{"id":1992,"depth":393,"text":868},{"id":2050,"depth":393,"text":2051},{"id":2145,"depth":393,"text":2146},{"id":2199,"depth":393,"text":2200},{"id":2419,"depth":393,"text":2420},{"id":2463,"depth":393,"text":2464,"children":3398},[3399],{"id":2566,"depth":465,"text":2567},{"id":2586,"depth":393,"text":2587},{"id":2614,"depth":393,"text":2615},{"id":2659,"depth":393,"text":2660},{"id":2759,"depth":393,"text":868},{"id":2800,"depth":393,"text":2801},{"id":2901,"depth":393,"text":3406},"5.2 코드 변경 (백엔드 — malgn-noti-api)",{"id":3025,"depth":393,"text":3408},"5.3 코드 변경 (프런트 — malgn-noti)",{"id":3094,"depth":393,"text":3095},{"id":3235,"depth":393,"text":3236},{"id":3270,"depth":393,"text":3271},{"id":3294,"depth":393,"text":3295},"md",{},true,"\u002Fhistory\u002Fhistory.20260601",{"title":5,"description":352},"history\u002Fhistory.20260601","3PzEYB6TSxOmwoBGbfh0CAvctYdbA4Buu7de5zZ6ajQ",1780639760808]