[{"data":1,"prerenderedAt":9848},["ShallowReactive",2],{"doc:\u002Fhistory\u002Fhistory.20260602":3},{"id":4,"title":5,"body":6,"description":1016,"extension":9842,"meta":9843,"navigation":1729,"path":9844,"seo":9845,"stem":9846,"__hash__":9847},"docs\u002Fhistory\u002Fhistory.20260602.md","2026-06-02 — WBS 3 트랙 분리 + 로그인 UX(고객사 ID 제거) + loginid 전역 UNIQUE + 휴대폰 OTP + 토스트 가시성 + NICE 통합인증 인프라",{"type":7,"value":8,"toc":9671},"minimark",[9,13,18,93,96,100,104,131,135,138,142,224,230,234,237,251,260,264,373,377,397,401,407,447,449,456,459,492,498,504,575,590,597,671,675,762,765,769,810,814,838,842,862,864,871,874,917,921,997,1000,1004,1010,1039,1044,1100,1104,1175,1179,1249,1253,1317,1321,1356,1360,1417,1421,1424,1438,1440,1444,1447,1462,1466,1474,1555,1567,1570,1574,1581,1626,1633,1640,1646,1656,1803,1807,1814,1818,1863,1867,1898,1902,1909,1911,1915,1918,1937,1941,2019,2023,2029,2036,2109,2115,2140,2154,2161,2247,2254,2323,2330,2339,2387,2390,2394,2401,2406,2442,2456,2468,2471,2475,2580,2583,2589,2592,2638,2642,2709,2713,2774,2776,2787,2790,2825,2829,2839,2941,2971,2985,2989,3086,3090,3294,3297,3402,3408,3412,3491,3498,3502,3533,3537,3578,3580,3584,3587,3618,3622,3693,3710,3714,3720,3750,3756,3760,3764,3794,3798,3815,3819,3854,3858,3861,3883,3886,3909,3912,3949,3953,4070,4073,4077,4120,4124,4190,4192,4199,4202,4233,4240,4246,4627,4654,4658,4661,4740,4743,4859,4866,4870,4958,4961,4965,4978,4982,5016,5018,5026,5029,5062,5069,5075,5110,5121,5170,5173,5179,5185,5389,5392,5426,5432,5456,5467,5474,5484,5489,5506,5509,5514,5518,5567,5571,5591,5595,5641,5643,5650,5653,5681,5685,5768,5772,5832,5836,5896,5901,5925,5929,5939,5943,5980,5982,5989,5992,6089,6100,6121,6171,6183,6187,6223,6227,6237,6312,6316,6321,6424,6432,6439,6446,6541,6555,6562,6566,6609,6616,6632,6759,6763,6845,6848,6852,6897,6901,6948,6950,6954,6957,7049,7056,7135,7168,7172,7179,7296,7329,7333,7337,7378,7387,7391,7395,7405,7409,7420,7431,7442,7446,7454,7458,7470,7481,7493,7497,7602,7616,7620,7695,7698,7702,7739,7743,7786,7788,7792,7795,7859,7863,7911,7924,7937,7941,7945,8143,8148,8152,8298,8307,8316,8320,8354,8357,8361,8369,8502,8506,8542,8546,8553,8567,8570,8572,8576,8579,8600,8604,8661,8665,8719,8723,8736,8740,8747,8749,8753,8756,8779,8783,8805,8812,8816,8821,8867,8870,8899,8903,8947,8951,9018,9021,9025,9054,9058,9088,9090,9094,9097,9124,9128,9131,9137,9165,9174,9177,9183,9191,9202,9205,9229,9233,9282,9289,9293,9303,9307,9311,9331,9335,9350,9354,9363,9368,9444,9447,9506,9509,9512,9543,9552,9556,9566,9570,9613,9617,9667],[10,11,5],"h1",{"id":12},"_2026-06-02-wbs-3-트랙-분리-로그인-ux고객사-id-제거-loginid-전역-unique-휴대폰-otp-토스트-가시성-nice-통합인증-인프라",[14,15,17],"h2",{"id":16},"한-줄-요약","한 줄 요약",[19,20,21,22,26,27,30,31,35,36,30,39,42,43,46,47,50,51,54,55,58,59,62,63,30,66,69,70,73,74,77,78,81,82,85,86,88,89,92],"p",{},"이번주 회원·인증 트랙 첫 날 5건 처리. ",[23,24,25],"strong",{},"(§1)"," WBS 3 트랙 분리(5-3A UI \u002F 5-3M 매트릭스 \u002F 5-3C 연동) — 진척 과대평가 문제 해소, Step 5 55%→40%. ",[23,28,29],{},"(§2)"," ",[32,33,34],"code",{},"POST \u002Fauth\u002Flogin-by-email"," 신설 + 로그인 화면 \"고객사 ID\" 필드 완전 제거 (Workers #10 \u002F Pages #52). ",[23,37,38],{},"(§3)",[32,40,41],{},"TB_USER.loginid"," 전역 UNIQUE 정합화 — ",[32,44,45],{},"0003"," 라이브 적용, 복수 매치 경로 + 회사 선택 UI 제거 (Workers #11 \u002F Pages #53). ",[23,48,49],{},"(§4)"," 휴대폰 OTP 라우트 (",[32,52,53],{},"\u002Fauth\u002Fphone-code\u002Fsend","·",[32,56,57],{},"\u002Fverify",") + signup.vue Step 4 SMS OTP 연동 + 로그인 401 처리 정합화(",[32,60,61],{},"\u002Fauth\u002F*"," 호출은 자동 리다이렉트 안 함) + 회원가입 완료 화면 고객사 ID 노출 제거 + 토스트 위치(오른쪽 위) + 크기 강화(17px). NHN_MOCK secret 적용 — 자격증명 발급 전 mock 통과. (Workers #12 \u002F Pages #54~#58). ",[23,64,65],{},"(§5)",[23,67,68],{},"NICE 통합인증(휴대폰 본인확인)"," 인프라 — ",[32,71,72],{},"doc\u002FNICE_AUTH.md"," 신규 정본 + ",[32,75,76],{},"0004_user_nice_auth.sql"," 라이브 적용(TB_NICE_AUTH + TB_USER에 ci\u002Fbirthdate\u002Fgender\u002Fnational_info\u002Fmobile_co + UNIQUE ci) + NICE 어댑터(mock\u002Freal, AES-256-GCM + PBKDF2 + HMAC) + 3 라우트(init\u002Fcallback\u002Fstatus) + ",[32,79,80],{},"\u002Fauth\u002Fsignup"," 확장(niceSession 검증·CI 중복 차단·NICE 결과로 이름·휴대폰·생년월일 덮어쓰기) + signup.vue Step 4 통째로 NICE 흐름으로 교체(\"본인 인증하기\" 버튼 + 폴링 + 결과 표시) + NICE_MOCK secret 적용. (Workers #13 \u002F Pages #60). 라이브 e2e 모두 통과. ",[23,83,84],{},"이메일 인증창"," 차단 UX 버그 발견·수정: useApi 401 핸들러가 모든 401을 \u002Flogin으로 리다이렉트해서 가입 도중 OTP 잘못 입력하면 페이지 이동되던 문제 → ",[32,87,61],{}," 호출의 401은 호출자가 처리하도록 분리. ",[23,90,91],{},"NHN 자격증명 미등록",": 메일 실 발송 0 — 가입 흐름은 NHN_MOCK + NICE_MOCK secret 켜진 mock 모드로 통과.",[94,95],"hr",{},[10,97,99],{"id":98},"_1-wbs-구조-개편-사용자단을-3-트랙ui-api-연동으로-분리-배포-51","§1. WBS 구조 개편 — 사용자단을 **3 트랙(UI \u002F API \u002F 연동)**으로 분리 (배포 #51)",[14,101,103],{"id":102},"한-줄","한 줄",[19,105,106,107,110,111,114,115,118,119,122,123,126,127,130],{},"\"화면 UI는 그렸지만 백엔드 연동은 안 됐는데 ✅로 표시돼 진척이 과대평가되는\" 문제를 해소. WBS의 5-3을 ",[23,108,109],{},"5-3A 화면 UI 구성","(목업 데이터로 페이지만 그리기) + ",[23,112,113],{},"5-3M 매트릭스","(도메인별 UI\u002FAPI\u002F연동 한눈에) + ",[23,116,117],{},"5-3C 화면 ↔ API 연동","(실 데이터 흐름) 3 트랙으로 분리. 5-3-15의 단일 \"백엔드 연동\" 항목을 16개 도메인별 5-3C-1~16으로 펼침(인증·계정 + 이메일 OTP 2개만 ✅, 나머지 14개 ⚪). 5-2 API 항목들은 그대로 두되 5-3M 매트릭스에서 도메인 단위로 매핑. Step 5 진척률을 55% → 40%로 재산정(연동 트랙 약 7%만 완료 반영) → 전체 가중평균 45% → 38%. ",[32,120,121],{},"doc\u002FWBS.md"," + ",[32,124,125],{},"app\u002Fpages\u002Fwbs.vue"," 양쪽 동기, Pages 배포 #51(alias ",[32,128,129],{},"bca573ce.malgn-noti.pages.dev",").",[14,132,134],{"id":133},"_11-문제","1.1 문제",[19,136,137],{},"5-3 항목들의 ✅는 사실상 모두 \"UI 화면을 목업 데이터로 그렸다\" 단계까지를 의미했는데, WBS만 보면 \"발송·이력·주소록 등이 모두 완료\"처럼 보였다. 6\u002F1 (어제 history.20260601.md) §4·§5에서 \"인증·계정만 실 API 연동 완료\"로 5-3-15(백엔드 연동)를 추가했지만, 도메인별 단위가 아니라 한 항목으로 묶여 있어 어디까지 됐고 어디가 안 됐는지가 가시화되지 않음.",[14,139,141],{"id":140},"_12-해결-3-트랙-분리","1.2 해결 — 3 트랙 분리",[143,144,145,164],"table",{},[146,147,148],"thead",{},[149,150,151,155,158,161],"tr",{},[152,153,154],"th",{},"트랙",[152,156,157],{},"항목 ID",[152,159,160],{},"의미",[152,162,163],{},"✅의 기준",[165,166,167,186,205],"tbody",{},[149,168,169,175,180,183],{},[170,171,172],"td",{},[23,173,174],{},"A. 화면 UI 구성",[170,176,177],{},[32,178,179],{},"5-3A-*",[170,181,182],{},"목업 데이터로 페이지 그리기",[170,184,185],{},"라우트가 라이브, 화면이 렌더링",[149,187,188,193,199,202],{},[170,189,190],{},[23,191,192],{},"B. API 엔드포인트",[170,194,195,198],{},[32,196,197],{},"5-2-*"," (기존)",[170,200,201],{},"백엔드 라우트 구현",[170,203,204],{},"라우트가 라이브, e2e 검증",[149,206,207,212,218,221],{},[170,208,209],{},[23,210,211],{},"C. 화면 ↔ API 연동",[170,213,214,217],{},[32,215,216],{},"5-3C-*"," (신규)",[170,219,220],{},"실 데이터 흐름 + 상태 관리 + 에러 처리",[170,222,223],{},"UI가 실 API를 호출, 응답이 화면에 반영",[19,225,226,227,229],{},"추가로 ",[23,228,113],{}," — 각 도메인을 한 행에 UI\u002FAPI\u002F연동 3 칸으로 정렬해 어디까지 됐는지 한눈에. 25 도메인 × 3 트랙 = 75 칸.",[14,231,233],{"id":232},"_13-5-3c-펼침-16-항목","1.3 5-3C 펼침 (16 항목)",[19,235,236],{},"5-3-15 단일 항목 → 도메인별 16 항목:",[238,239,240,244],"ul",{},[241,242,243],"li",{},"✅ 2개: 5-3C-1 (인증·계정), 5-3C-1a (이메일 OTP)",[241,245,246,247,250],{},"⚪ 14개: 로그아웃·비밀번호 재설정·login-by-email·약관 동의·companyType·",[32,248,249],{},"\u002Fme"," 갱신·비밀번호 변경·2FA·멀티 계정·계약·발송 6채널·이력\u002F통계·주소록 등 CRUD·결제·문의",[19,252,253,254,259],{},"우선순위는 ",[255,256,258],"a",{"href":257},"..\u002FMEMBERSHIP","doc\u002FMEMBERSHIP.md"," §8과 일치 — P0 3, P1 4, P2 4, P3 3.",[14,261,263],{"id":262},"_14-진척률-재산정","1.4 진척률 재산정",[143,265,266,282],{},[146,267,268],{},[149,269,270,273,276,279],{},[152,271,272],{},"Step",[152,274,275],{},"기존",[152,277,278],{},"재산정",[152,280,281],{},"사유",[165,283,284,297,308,320,332,351],{},[149,285,286,289,292,294],{},[170,287,288],{},"1 준비",[170,290,291],{},"55%",[170,293,291],{},[170,295,296],{},"변동 없음",[149,298,299,302,304,306],{},[170,300,301],{},"2 정책",[170,303,291],{},[170,305,291],{},[170,307,296],{},[149,309,310,313,316,318],{},[170,311,312],{},"3 기획",[170,314,315],{},"35%",[170,317,315],{},[170,319,296],{},[149,321,322,325,328,330],{},[170,323,324],{},"4 디자인",[170,326,327],{},"20%",[170,329,327],{},[170,331,296],{},[149,333,334,339,343,348],{},[170,335,336],{},[23,337,338],{},"5 개발",[170,340,341],{},[23,342,291],{},[170,344,345],{},[23,346,347],{},"40%",[170,349,350],{},"UI(거의 완료) + API(60%) + 연동(7%)을 가중평균. UI는 7주의 작업이고 연동·API가 더 큰 비중을 차지하므로 단순 평균",[149,352,353,358,363,368],{},[170,354,355],{},[23,356,357],{},"전체 가중평균",[170,359,360],{},[23,361,362],{},"45%",[170,364,365],{},[23,366,367],{},"38%",[170,369,370],{},[32,371,372],{},"0.10×55 + 0.15×55 + 0.20×35 + 0.10×20 + 0.45×40 ≈ 37.75",[14,374,376],{"id":375},"_15-산출물","1.5 산출물",[238,378,379,385,391],{},[241,380,381,384],{},[255,382,121],{"href":383},"..\u002FWBS"," — 5-3 섹션 전면 개편 (5-3A·5-3M·5-3C). 진척률 스냅샷 갱신. 가중평균 45→38.",[241,386,387,390],{},[255,388,125],{"href":389},"..\u002F..\u002Fapp\u002Fpages\u002Fwbs.vue"," — group 라벨 '사용자단 화면' → '사용자단 화면 UI (목업)', 5-3-15 삭제 + 5-3C-* 16 신규, stage-5 progress 55→40 + summary 갱신.",[241,392,393,394,396],{},"Pages 배포 #51 (alias ",[32,395,129],{},"). 라이브 그렙으로 17개 5-3C 항목 + 새 그룹 라벨 2종 노출 확인.",[14,398,400],{"id":399},"_16-다음-작업-이번주-회원인증-트랙","1.6 다음 작업 (이번주 회원·인증 트랙)",[19,402,403,406],{},[255,404,405],{"href":257},"MEMBERSHIP.md"," §8 P0 3건 + P1 4건이 이번주 본격 작업. 가장 빠른 영향 순:",[408,409,410,416,422,431,437],"ol",{},[241,411,412,415],{},[23,413,414],{},"5-3C-2 로그아웃 GNB 실 연결"," (30분, 의존 0)",[241,417,418,421],{},[23,419,420],{},"5-3C-3 비밀번호 재설정"," (2~3시간, OTP 인프라 재활용)",[241,423,424,430],{},[23,425,426,427],{},"5-3C-4 ",[32,428,429],{},"\u002Fauth\u002Flogin-by-email"," (1~2시간, companyId UX 개선)",[241,432,433,436],{},[23,434,435],{},"5-3C-5 약관 동의 적재"," (1~2시간)",[241,438,439,446],{},[23,440,441,442,445],{},"5-3C-6 ",[32,443,444],{},"companyType"," 전달·저장"," (2~3시간) + 5-3C-6 따라가는 개인 유형 화면 분기 (30분)",[94,448],{},[10,450,452,453,455],{"id":451},"_2-로그인-ux-개선-post-authlogin-by-email-고객사-id-필드-제거-배포-1052","§2. 로그인 UX 개선 — ",[32,454,34],{}," + 고객사 ID 필드 제거 (배포 #10·#52)",[14,457,103],{"id":458},"한-줄-1",[19,460,461,462,465,466,468,469,472,473,476,477,480,481,484,485,488,489,130],{},"(어제) §4의 알려진 한계(\"로그인이 ",[32,463,464],{},"companyId","를 요구해 사용자가 자신의 회사 ID를 외워야 함\")를 해소. 백엔드 ",[32,467,34],{}," 신설 — 이메일(또는 아이디) + 비밀번호만으로 회사 자동 찾기, 단일 매치 시 즉시 토큰 발급, 같은 이메일로 여러 회사에 가입된 경우 ",[32,470,471],{},"multipleCompanies: true + companies[]"," 반환. 프런트 ",[32,474,475],{},"login\u002Findex.vue","에서 ",[23,478,479],{},"고객사 ID 필드를 완전히 제거",", 복수 매치 시 회사 선택 카드 UI 노출 → 선택 시 기존 ",[32,482,483],{},"\u002Fauth\u002Flogin","으로 명시적 로그인. 라이브 e2e 5 시나리오 통과(단일\u002F복수\u002F잘못된 비번\u002F없는 이메일\u002F같은 이메일 2회사). Workers 배포 #10(Version ",[32,486,487],{},"a6197cc7-0f01-4612-aa10-5271f7c494a1","), Pages 배포 #52(alias ",[32,490,491],{},"292da05d.malgn-noti.pages.dev",[14,493,495,496],{"id":494},"_21-백엔드-post-authlogin-by-email","2.1 백엔드 — ",[32,497,34],{},[19,499,500,503],{},[32,501,502],{},"src\u002Froutes\u002Fauth.ts",":",[238,505,506,528,535,538,555,565],{},[241,507,508,509,512,513,516,517,520,521,523,524,527],{},"입력: ",[32,510,511],{},"{email, password}"," — ",[32,514,515],{},"email"," 필드명이지만 실제로는 ",[32,518,519],{},"loginid"," 또는 ",[32,522,515],{}," 컬럼 매치 (회원가입 마법사가 ",[32,525,526],{},"loginid = email","로 발급하므로 둘 다 검색)",[241,529,530,531,534],{},"검색: ",[32,532,533],{},"WHERE user.status=1 AND company.status=1 AND (user.loginid = ? OR user.email = ?)"," + INNER JOIN company",[241,536,537],{},"각 row별로 PBKDF2 비번 검증 (서로 다른 회사·다른 비밀번호 가능)",[241,539,540,543,544,546,547,550,551,554],{},[23,541,542],{},"단일 매치",": 기존 ",[32,545,483],{},"과 동일 형식의 ",[32,548,549],{},"AuthResponse"," 반환 + 토큰 발급 + ",[32,552,553],{},"lastLoginAt"," 갱신",[241,556,557,560,561,564],{},[23,558,559],{},"복수 매치",": ",[32,562,563],{},"{multipleCompanies: true, companies: [{id, name}, ...]}"," 반환 (토큰 발급 안 함)",[241,566,567,570,571,574],{},[23,568,569],{},"매치 0 또는 비번 모두 불일치",": 401 ",[32,572,573],{},"unauthenticated"," (계정 enumeration 방지)",[19,576,577,578,581,582,585,586,589],{},"OpenAPI: 신규 path 1 + 신규 schema 2(",[32,579,580],{},"LoginByEmailRequest",", ",[32,583,584],{},"MultipleCompaniesResponse","). 응답 schema는 ",[32,587,588],{},"oneOf: [AuthResponse, MultipleCompaniesResponse]"," — 두 가지 가능 형태 명시.",[14,591,593,594,596],{"id":592},"_22-프런트-loginindexvue-개편","2.2 프런트 — ",[32,595,475],{}," 개편",[238,598,599,619,639,655,661],{},[241,600,601,604,605,54,608,54,611,614,615,618],{},[23,602,603],{},"고객사 ID 필드 완전 제거",". 5\u002F27 §12에서 도입한 ",[32,606,607],{},"companyIdInput",[32,609,610],{},"needCompanyId",[32,612,613],{},"effectiveCompanyId"," 로직 모두 삭제. ",[32,616,617],{},"last-company-id"," 쿠키도 더 이상 로그인 폼에서 사용하지 않음(다만 인증 후 hydrateFromAuth에서 갱신은 유지 — 이전 가입 흔적 보존).",[241,620,621,626,627,630,631,634,635,638],{},[23,622,623],{},[32,624,625],{},"stores\u002Fauth.ts.loginByEmail()"," 액션 신규 — 반환값 ",[32,628,629],{},"null"," = 단일 매치 (로그인 완료) \u002F ",[32,632,633],{},"{id,name}[]"," = 복수 매치 (호출자가 회사 선택 후 ",[32,636,637],{},"login()"," 재호출).",[241,640,641,560,644,647,648,651,652,654],{},[23,642,643],{},"복수 매치 UI",[32,645,646],{},"companyChoices"," ref가 비어있지 않으면 일반 폼 대신 회사 선택 카드 리스트 노출. 카드 클릭 시 ",[32,649,650],{},"chooseCompany(companyId)"," → 기존 ",[32,653,637],{}," 호출. \"다시 입력\" 버튼으로 초기 폼 복귀.",[241,656,657,660],{},[23,658,659],{},"에러 처리",": 401 응답 → \"아이디 또는 비밀번호가 올바르지 않습니다.\" 토스트. 그 외 → \"로그인 중 오류가 발생했습니다.\"",[241,662,663,666,667,670],{},[23,664,665],{},"이메일 placeholder",": \"아이디를 입력해 주세요\" → \"가입 시 사용한 이메일을 입력해 주세요\" + ",[32,668,669],{},"inputmode=\"email\""," 힌트.",[14,672,674],{"id":673},"_23-라이브-e2e-production","2.3 라이브 e2e (Production)",[143,676,677,690],{},[146,678,679],{},[149,680,681,684,687],{},[152,682,683],{},"#",[152,685,686],{},"시나리오",[152,688,689],{},"결과",[165,691,692,703,713,725,735,748],{},[149,693,694,697,700],{},[170,695,696],{},"1",[170,698,699],{},"signup → company.id=12 발급",[170,701,702],{},"✅",[149,704,705,708,711],{},[170,706,707],{},"2",[170,709,710],{},"login-by-email 단일 매치 → 200 + token (169자)",[170,712,702],{},[149,714,715,718,723],{},[170,716,717],{},"3",[170,719,720,721],{},"잘못된 비밀번호 → 401 ",[32,722,573],{},[170,724,702],{},[149,726,727,730,733],{},[170,728,729],{},"4",[170,731,732],{},"존재하지 않는 이메일 → 401 (계정 enumeration 방지)",[170,734,702],{},[149,736,737,740,746],{},[170,738,739],{},"5",[170,741,742,743],{},"같은 이메일로 2번째 회사 signup → login-by-email → ",[32,744,745],{},"{multipleCompanies:true, companies:[{id:12,name:...}, {id:13,name:...}]}",[170,747,702],{},[149,749,750,753,760],{},[170,751,752],{},"6",[170,754,755,756,759],{},"프로덕션 ",[32,757,758],{},"\u002Flogin"," 페이지 그렙 — \"고객사 ID\" 0건 \u002F \"가입 시 사용한 이메일\" 1건",[170,761,702],{},[19,763,764],{},"검증 과정의 임시 계정(company.id 12·13) 2건은 SG 재개방 시 cleanup 예정.",[14,766,768],{"id":767},"_24-산출물","2.4 산출물",[238,770,771,785,796,807],{},[241,772,773,774,776,777,780,781,784],{},"API: 3 파일 수정 — ",[32,775,502],{},"(+85) · ",[32,778,779],{},"src\u002Fopenapi.ts","(+25) · ",[32,782,783],{},"src\u002Fdb\u002Fschema.ts"," (변동 없음 — verification 정의는 §5에서 이미 반영).",[241,786,787,788,791,792,795],{},"사용자단: 2 파일 수정 — ",[32,789,790],{},"app\u002Fstores\u002Fauth.ts","(+20) · ",[32,793,794],{},"app\u002Fpages\u002Flogin\u002Findex.vue","(전면 개편, +90\u002F-30).",[241,797,798,799,802,803,806],{},"Workers 배포 #10 Version ",[32,800,801],{},"a6197cc7...",", Pages 배포 #52 alias ",[32,804,805],{},"292da05d",".",[241,808,809],{},"WBS 5-3C-4 ⚪ → ✅. doc\u002FMEMBERSHIP.md §8 P0 #3 완료(로그아웃·재설정 다음).",[14,811,813],{"id":812},"_25-보안-노트","2.5 보안 노트",[238,815,816,826,832],{},[241,817,818,560,821,520,823,825],{},[23,819,820],{},"로그인 가능한 입력",[32,822,519],{},[32,824,515],{}," 컬럼 매치. 같은 사용자가 두 컬럼에 다른 값을 가질 수 있다면(현재 회원가입 마법사는 둘 다 email로 채움) 둘 다로 로그인 가능. 운영상 의도된 동작.",[241,827,828,831],{},[23,829,830],{},"enumeration 방지",": 잘못된 이메일·잘못된 비밀번호 모두 동일한 401 메시지(\"Authentication required\") — 응답 내용으로 이메일 존재 여부를 알 수 없음.",[241,833,834,837],{},[23,835,836],{},"타이밍",": 매치 row 수만큼 PBKDF2를 돌리므로 row 수가 많으면 응답 시간이 살짝 길어짐. 복수 매치는 실제로는 드물지만, 한 이메일을 의도적으로 많이 등록해 DoS 가능. 후속 rate limit 작업과 함께 검토.",[14,839,841],{"id":840},"_26-알려진-한계","2.6 알려진 한계",[238,843,844,856],{},[241,845,846,851,852,855],{},[23,847,848,850],{},[32,849,617],{}," 쿠키 잔존",": 더 이상 로그인 폼에서 사용하지 않으나, ",[32,853,854],{},"hydrateFromAuth","에서 여전히 갱신. 후속에서 제거 또는 다른 용도로 활용 검토.",[241,857,858,861],{},[23,859,860],{},"회원가입에서 loginid ≠ email로 가입한 사용자",": 현재 마법사 외 경로(예: 운영자단 강제 가입)로 만들어진 사용자는 이메일이 비어 있을 수 있어 login-by-email로 로그인 불가. 운영자단 흐름이 생기면 정책 정의 필요.",[94,863],{},[10,865,867,868,870],{"id":866},"_3-tb_userloginid-전역-unique-정책-정합화-배포-1153","§3. ",[32,869,41],{}," 전역 UNIQUE — 정책 정합화 (배포 #11·#53)",[14,872,103],{"id":873},"한-줄-2",[19,875,876,877,880,881,884,885,888,889,892,893,896,897,900,901,904,905,907,908,910,911,913,914,916],{},"(§2에서) 도입한 ",[32,878,879],{},"login-by-email","의 \"복수 매치\" 경로는 사실 ",[32,882,883],{},"UNIQUE (company_id, loginid)"," 복합 제약 때문에 같은 loginid가 회사별로 따로 존재할 수 있다는 가정에서 나왔는데, 사용자 정책 결정으로 ",[23,886,887],{},"loginid는 회사와 무관하게 전체 시스템에서 유일해야 함","으로 정리. DDL 마이그레이션 ",[32,890,891],{},"0003_user_loginid_global_unique.sql"," 라이브 적용(",[32,894,895],{},"uq_user_company_loginid"," DROP → ",[32,898,899],{},"uq_user_loginid"," ADD), schema.ts에 ",[32,902,903],{},".unique('uq_user_loginid')"," 명시, 백엔드 ",[32,906,429],{},"의 복수 매치 분기 제거, OpenAPI에서 ",[32,909,584],{}," 스키마 삭제, 프런트 ",[32,912,625],{}," 반환 타입 단순화 + ",[32,915,475],{},"에서 회사 선택 카드 UI 80여 라인 제거. 라이브 e2e 4 시나리오 통과(signup 정상 \u002F 같은 loginid 재시도 409 \u002F login-by-email 단일 토큰 \u002F multipleCompanies 응답 사라짐). 사전 테스트 데이터 cleanup 8개 회사 + 12개 사용자(어제 검증용 임시 계정).",[14,918,920],{"id":919},"_31-정책-변경","3.1 정책 변경",[143,922,923,936],{},[146,924,925],{},[149,926,927,930,933],{},[152,928,929],{},"항목",[152,931,932],{},"변경 전",[152,934,935],{},"변경 후",[165,937,938,955,969,984],{},[149,939,940,943,949],{},[170,941,942],{},"TB_USER UNIQUE",[170,944,945,948],{},[32,946,947],{},"(company_id, loginid)"," 복합",[170,950,951,954],{},[32,952,953],{},"(loginid)"," 단독",[149,956,957,960,963],{},[170,958,959],{},"같은 이메일로 여러 회사 가입",[170,961,962],{},"가능",[170,964,965,968],{},[23,966,967],{},"불가"," — signup 시 409 conflict",[149,970,971,976,979],{},[170,972,973,975],{},[32,974,879],{}," 응답 분기",[170,977,978],{},"단일\u002F복수",[170,980,981],{},[23,982,983],{},"단일만",[149,985,986,989,992],{},[170,987,988],{},"회사 선택 UI",[170,990,991],{},"복수 매치 시 카드 리스트",[170,993,994],{},[23,995,996],{},"삭제",[19,998,999],{},"이로써 \"한 이메일 = 한 회사 = 한 로그인\"이 보장됨. 멀티 계정(주계정·보조계정)은 같은 회사 내 다른 loginid로 처리.",[14,1001,1003],{"id":1002},"_32-ddl-마이그레이션-라이브-적용-완료","3.2 DDL 마이그레이션 (라이브 적용 완료)",[19,1005,1006,503],{},[255,1007,1009],{"href":1008},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Fdb\u002Fmigrations\u002F0003_user_loginid_global_unique.sql","src\u002Fdb\u002Fmigrations\u002F0003_user_loginid_global_unique.sql",[1011,1012,1017],"pre",{"className":1013,"code":1014,"language":1015,"meta":1016,"style":1016},"language-sql shiki shiki-themes github-light github-dark","ALTER TABLE TB_USER\n  DROP INDEX uq_user_company_loginid,\n  ADD UNIQUE KEY uq_user_loginid (loginid);\n","sql","",[32,1018,1019,1027,1033],{"__ignoreMap":1016},[1020,1021,1024],"span",{"class":1022,"line":1023},"line",1,[1020,1025,1026],{},"ALTER TABLE TB_USER\n",[1020,1028,1030],{"class":1022,"line":1029},2,[1020,1031,1032],{},"  DROP INDEX uq_user_company_loginid,\n",[1020,1034,1036],{"class":1022,"line":1035},3,[1020,1037,1038],{},"  ADD UNIQUE KEY uq_user_loginid (loginid);\n",[1040,1041,1043],"h3",{"id":1042},"적용-순서-sg-열린-짧은-윈도우-활용","적용 순서 (SG 열린 짧은 윈도우 활용)",[408,1045,1046,1072,1078],{},[241,1047,1048,1051,1052],{},[23,1049,1050],{},"사전 cleanup"," — 어제부터 누적된 검증용 임시 계정 정리:\n",[238,1053,1054,1060,1066],{},[241,1055,1056,1059],{},[32,1057,1058],{},"TB_USER"," 6 → 4 (lbe 중복 2건 + hd-check + ddl 등)",[241,1061,1062,1065],{},[32,1063,1064],{},"TB_COMPANY"," 그에 맞춰 정리",[241,1067,1068,1071],{},[32,1069,1070],{},"TB_VERIFICATION"," 0건",[241,1073,1074,1077],{},[23,1075,1076],{},"DDL 적용"," — mysql CLI 직결로 ALTER 실행, exit=0",[241,1079,1080,1083,1084],{},[23,1081,1082],{},"사후 검증",":\n",[238,1085,1086,1093],{},[241,1087,1088,1089,1092],{},"인덱스 확인: ",[32,1090,1091],{},"uq_user_loginid (loginid)"," 단독 노출",[241,1094,1095,1096,1099],{},"중복 INSERT 시도: ",[32,1097,1098],{},"Duplicate entry … for key 'TB_USER.uq_user_loginid'"," 1062 에러 → ✅ 동작",[14,1101,1103],{"id":1102},"_33-코드-변경-백엔드","3.3 코드 변경 (백엔드)",[143,1105,1106,1116],{},[146,1107,1108],{},[149,1109,1110,1113],{},[152,1111,1112],{},"파일",[152,1114,1115],{},"변경",[165,1117,1118,1132,1153],{},[149,1119,1120,1125],{},[170,1121,1122],{},[255,1123,783],{"href":1124},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Fdb\u002Fschema.ts",[170,1126,1127,1128,1131],{},"TB_USER 정의에 ",[32,1129,1130],{},"loginid: varchar(...).notNull().unique('uq_user_loginid')"," 추가 + 헤더 코멘트",[149,1133,1134,1139],{},[170,1135,1136],{},[255,1137,502],{"href":1138},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Froutes\u002Fauth.ts",[170,1140,1141,1144,1145,1148,1149,1152],{},[32,1142,1143],{},"\u002Flogin-by-email"," 단순화 — ",[32,1146,1147],{},"for of"," 다중 verify 루프 → ",[32,1150,1151],{},".limit(1)"," 단일 select + 단일 password check. 복수 매치 분기 + multipleCompanies 응답 코드 삭제",[149,1154,1155,1160],{},[170,1156,1157],{},[255,1158,779],{"href":1159},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Fopenapi.ts",[170,1161,1162,1164,1165,1167,1168,1171,1172,1174],{},[32,1163,584],{}," 스키마 삭제. ",[32,1166,1143],{}," 응답 ",[32,1169,1170],{},"oneOf"," → 단일 ",[32,1173,549],{},"로 단순화. 설명 갱신(\"loginid 전역 UNIQUE — 최대 1건 매치\").",[14,1176,1178],{"id":1177},"_34-코드-변경-프런트","3.4 코드 변경 (프런트)",[143,1180,1181,1189],{},[146,1182,1183],{},[149,1184,1185,1187],{},[152,1186,1112],{},[152,1188,1115],{},[165,1190,1191,1206],{},[149,1192,1193,1198],{},[170,1194,1195],{},[255,1196,790],{"href":1197},"..\u002F..\u002Fapp\u002Fstores\u002Fauth.ts",[170,1199,1200,1203,1204],{},[32,1201,1202],{},"loginByEmail()"," 반환 타입 `Promise\u003CCompany",[1020,1205],{},[149,1207,1208,1213],{},[170,1209,1210],{},[255,1211,794],{"href":1212},"..\u002F..\u002Fapp\u002Fpages\u002Flogin\u002Findex.vue",[170,1214,1215,1217,1218,1221,1222,1225,1226,1229,1230,54,1233,54,1236,54,1239,54,1242,54,1245,1248],{},[32,1216,646],{}," ref \u002F ",[32,1219,1220],{},"showCompanyPicker"," computed \u002F ",[32,1223,1224],{},"chooseCompany()"," \u002F ",[32,1227,1228],{},"cancelCompanyPick()"," 함수 + 회사 선택 카드 템플릿 + 관련 스타일 (",[32,1231,1232],{},".picker-desc",[32,1234,1235],{},".company-list",[32,1237,1238],{},".company-card",[32,1240,1241],{},".company-name",[32,1243,1244],{},".company-id",[32,1246,1247],{},".company-arrow",") 모두 삭제. 화면은 단일 폼만.",[14,1250,1252],{"id":1251},"_35-라이브-e2e-production","3.5 라이브 e2e (Production)",[143,1254,1255,1265],{},[146,1256,1257],{},[149,1258,1259,1261,1263],{},[152,1260,683],{},[152,1262,686],{},[152,1264,689],{},[165,1266,1267,1280,1293,1306],{},[149,1268,1269,1271,1278],{},[170,1270,696],{},[170,1272,1273,1274,1277],{},"signup → ",[32,1275,1276],{},"{user, company, token}"," 정상 (company.id=14, user.id=16)",[170,1279,702],{},[149,1281,1282,1284,1291],{},[170,1283,707],{},[170,1285,1286,1287,1290],{},"같은 loginid로 두 번째 signup → 409 ",[32,1288,1289],{},"conflict"," \"loginid \"…\" 이미 사용 중\"",[170,1292,702],{},[149,1294,1295,1297,1304],{},[170,1296,717],{},[170,1298,1299,1300,1303],{},"login-by-email 정상 매치 → 200 + 단일 토큰. 응답에 ",[32,1301,1302],{},"multipleCompanies"," 키 없음",[170,1305,702],{},[149,1307,1308,1310,1315],{},[170,1309,729],{},[170,1311,1312,1313,954],{},"cleanup 후 인덱스 확인 — UNIQUE 인덱스 ",[32,1314,1091],{},[170,1316,702],{},[14,1318,1320],{"id":1319},"_36-산출물","3.6 산출물",[238,1322,1323,1332,1341,1350],{},[241,1324,1325,560,1328,1331],{},[23,1326,1327],{},"DDL",[32,1329,1330],{},"malgn-noti-api\u002Fsrc\u002Fdb\u002Fmigrations\u002F0003_user_loginid_global_unique.sql"," 신규 + 라이브 적용",[241,1333,1334,1337,1338],{},[23,1335,1336],{},"API",": 3 파일 수정 — schema.ts · auth.ts · openapi.ts. Workers 배포 #11 Version ",[32,1339,1340],{},"f7f42855-1d40-4397-9405-df8bfa8124ee",[241,1342,1343,1346,1347],{},[23,1344,1345],{},"사용자단",": 2 파일 수정 — stores\u002Fauth.ts(-25) · login\u002Findex.vue(-80). Pages 배포 #53 alias ",[32,1348,1349],{},"f150ea0a.malgn-noti.pages.dev",[241,1351,1352,1355],{},[23,1353,1354],{},"데이터 정리",": 어제~오늘 누적된 검증용 임시 회사 8 + 사용자 12 + verification 미소비분 cleanup",[14,1357,1359],{"id":1358},"_37-영향-분석-다른-코드에-미치는-영향","3.7 영향 분석 — 다른 코드에 미치는 영향",[143,1361,1362,1371],{},[146,1363,1364],{},[149,1365,1366,1368],{},[152,1367,929],{},[152,1369,1370],{},"영향",[165,1372,1373,1384,1393,1401,1409],{},[149,1374,1375,1381],{},[170,1376,1377,1378,1380],{},"기존 ",[32,1379,483],{}," (companyId+loginid)",[170,1382,1383],{},"그대로 동작 — companyId가 제약을 더 좁히지만 결과는 같음",[149,1385,1386,1390],{},[170,1387,1388],{},[32,1389,80],{},[170,1391,1392],{},"catch 블록의 \"Duplicate entry\" 메시지 매핑 그대로 (에러 메시지 자체가 회사·loginid 어느 키든 같은 형태)",[149,1394,1395,1398],{},[170,1396,1397],{},"멀티계정(주·보조 사용자)",[170,1399,1400],{},"같은 회사 내에서 서로 다른 loginid를 사용 — 영향 없음",[149,1402,1403,1406],{},[170,1404,1405],{},"운영자단 강제 가입",[170,1407,1408],{},"미구현 — 정책 정의 시 전역 UNIQUE 전제로 시작",[149,1410,1411,1414],{},[170,1412,1413],{},"OTP \u002F 비밀번호 재설정",[170,1415,1416],{},"email\u002Floginid 기반 lookup — 단일 매치 보장으로 단순화 가능 (후속)",[14,1418,1420],{"id":1419},"_38-다음-단계","3.8 다음 단계",[19,1422,1423],{},"지금 정책이 정리됐으니 다음 P0 항목들이 한층 단순해집니다:",[238,1425,1426,1433],{},[241,1427,1428,512,1430,1432],{},[23,1429,420],{},[32,1431,515],{},"로 lookup하면 단일 사용자 → 토큰 발급도 단순. OTP 인프라 재활용 → 2시간 이내 가능.",[241,1434,1435,1437],{},[23,1436,414],{}," — 정책 변경과 무관, 30분.",[94,1439],{},[10,1441,1443],{"id":1442},"_4-휴대폰-sms-otp-로그인-401-처리-가입-완료-id-노출-제거-토스트-가시성-배포-12-5458","§4. 휴대폰 SMS OTP + 로그인 401 처리 + 가입 완료 ID 노출 제거 + 토스트 가시성 (배포 #12 \u002F #54~#58)",[14,1445,103],{"id":1446},"한-줄-3",[19,1448,1449,1450,1453,1454,1457,1458,1461],{},"이메일 OTP 인프라(",[32,1451,1452],{},"(어제) §5",") 후속 — ",[23,1455,1456],{},"휴대폰 SMS OTP","를 같은 패턴으로 추가하여 signup.vue Step 4를 실 API로 일관 연결 + 가입 도중 발견된 4개 UX 이슈(401 자동 리다이렉트, 가입 완료 화면의 고객사 ID 노출, 토스트 위치, 토스트 크기) 정리. Workers 배포 #12(Version ",[32,1459,1460],{},"84056c86...","), Pages 배포 #54~#58. 자체 SMS OTP는 단순 휴대폰 보유 검증으로 유지 — 본인 확인(이름·CI 등)은 §5 NICE로 분리.",[14,1463,1465],{"id":1464},"_41-휴대폰-otp-라우트-이메일과-동일-패턴","4.1 휴대폰 OTP 라우트 — 이메일과 동일 패턴",[19,1467,1468,122,1471,503],{},[32,1469,1470],{},"POST \u002Fauth\u002Fphone-code\u002Fsend",[32,1472,1473],{},"POST \u002Fauth\u002Fphone-code\u002Fverify",[238,1475,1476,1489,1492,1495,1510,1521,1532,1542],{},[241,1477,1478,1480,1481,1484,1485,1488],{},[32,1479,1070],{},"에 ",[32,1482,1483],{},"target_type='phone'"," 적재 (이메일은 ",[32,1486,1487],{},"'email'",")",[241,1490,1491],{},"SHA-256(target|purpose|code) 해시 — 평문 코드 저장 금지",[241,1493,1494],{},"TTL 10분 · 재발송 시 직전 코드 만료 · 5회 시도 제한 · 소비 후 재사용 차단",[241,1496,1497,1500,1501,1225,1504,1225,1507],{},[32,1498,1499],{},"purpose"," enum 확장: ",[32,1502,1503],{},"signup",[32,1505,1506],{},"reset_password",[32,1508,1509],{},"change_phone",[241,1511,1512,1513,1516,1517,1520],{},"휴대폰 번호 정규화: 입력값에서 숫자만 추출(",[32,1514,1515],{},"010-1234-5678"," → ",[32,1518,1519],{},"01012345678",") — 같은 사용자의 다른 표기를 같은 코드 한 건으로 매핑",[241,1522,1523,1524,1527,1528,1531],{},"SMS 발송은 NHN SMS 어댑터 (mock\u002Freal). ",[32,1525,1526],{},"NHN_MOCK=1"," 또는 자격증명 미설정 시 mock fallback. mock 모드면 응답에 ",[32,1529,1530],{},"mockCode"," 노출(개발 편의)",[241,1533,1534,1537,1538,1541],{},[32,1535,1536],{},"OtpPurpose"," 타입 확장 + ",[32,1539,1540],{},"purposeLabel()"," 4개 분기",[241,1543,1544,1547,1548,1551,1552,1488],{},[32,1545,1546],{},"EMAIL_FROM"," 외 ",[32,1549,1550],{},"SMS_FROM"," env var 추가 (기본 ",[32,1553,1554],{},"01000000000",[19,1556,1557,1558,54,1561,54,1564,130],{},"OpenAPI 4지점 추가(2 paths + 2 schemas ",[32,1559,1560],{},"PhoneCodeSendRequest",[32,1562,1563],{},"PhoneCodeSendResponse",[32,1565,1566],{},"PhoneCodeVerifyRequest",[19,1568,1569],{},"라이브 e2e 5+1 시나리오 통과: 발송 mockCode 노출 \u002F 잘못된 코드 401 \u002F 올바른 코드 200 \u002F 소비 후 재시도 401 \u002F 하이픈 포함 입력 정규화 \u002F 이메일 OTP도 같이 회복.",[14,1571,1573],{"id":1572},"_42-프런트-signupvue-step-4-실-api-연동-nice-도입-전-중간-단계","4.2 프런트 signup.vue Step 4 — 실 API 연동 (NICE 도입 전 중간 단계)",[19,1575,1576,1577,1580],{},"기존 화면 더미(",[32,1578,1579],{},"codeSent.value=true"," 토스트만) → 실 호출:",[238,1582,1583,1607,1618],{},[241,1584,1585,1516,1588,1590,1591,1594,1595,1597,1598,1225,1601,1225,1604,1488],{},[32,1586,1587],{},"sendCode()",[32,1589,1470],{}," async + ",[32,1592,1593],{},"sendingPhone"," 로딩 + ",[32,1596,1530],{}," 응답 시 토스트 노출 + 버튼 라벨 3-상태(",[32,1599,1600],{},"발송 중…",[32,1602,1603],{},"재발송",[32,1605,1606],{},"인증번호 받기",[241,1608,1609,1516,1612,1590,1614,1617],{},[32,1610,1611],{},"confirmCode()",[32,1613,1473],{},[32,1615,1616],{},"verifyingPhone"," 로딩 + 백엔드 한국어 에러 메시지 그대로 토스트",[241,1619,1620,1623,1624,1488],{},[32,1621,1622],{},"fullPhoneE164"," computed — 하이픈 제거(",[32,1625,1519],{},[19,1627,1628,1629,1632],{},"이 작업은 §5 NICE 도입 시점에 ",[23,1630,1631],{},"다시 통째로 교체됨","(NICE가 휴대폰 인증을 대신 수행). 백엔드 휴대폰 OTP 라우트는 비밀번호 재설정·휴대폰 번호 변경 등 후속 흐름에서 그대로 재활용.",[14,1634,1636,1637,1639],{"id":1635},"_43-useapits-401-처리-분리-auth는-호출자가-처리","4.3 useApi.ts 401 처리 분리 — ",[32,1638,61],{},"는 호출자가 처리",[19,1641,1642,1643,1645],{},"가입 중 이메일 OTP 잘못 입력 → 401 → useApi 핸들러가 ",[32,1644,758],{},"으로 리다이렉트 → 사용자가 코드 재입력 못 함 → 가입 흐름 차단.",[19,1647,1648,1649,30,1653,503],{},"수정 ",[255,1650,1652],{"href":1651},"..\u002F..\u002Fapp\u002Fcomposables\u002FuseApi.ts","app\u002Fcomposables\u002FuseApi.ts",[32,1654,1655],{},"onResponseError",[1011,1657,1661],{"className":1658,"code":1659,"language":1660,"meta":1016,"style":1016},"language-ts shiki shiki-themes github-light github-dark","const url = typeof request === 'string' ? request : (request as { url?: string }).url ?? ''\n\n\u002F\u002F \u002Fauth\u002F* 라우트의 401은 정상적인 \"잘못된 자격증명·OTP\" → 호출자가 처리해야 함\nif (url.includes('\u002Fauth\u002F')) return\n\n\u002F\u002F 인증되지 않은 상태에서 보호 라우트 호출 → 의미 있는 리다이렉트 아님\nif (!useAuthToken().value) return\n\n\u002F\u002F 인증된 상태 + 보호 라우트 401 → 토큰 만료 → \u002Flogin\n","ts",[32,1662,1663,1725,1731,1737,1762,1767,1773,1792,1797],{"__ignoreMap":1016},[1020,1664,1665,1669,1673,1676,1679,1683,1686,1690,1693,1695,1697,1700,1703,1706,1710,1713,1716,1719,1722],{"class":1022,"line":1023},[1020,1666,1668],{"class":1667},"szBVR","const",[1020,1670,1672],{"class":1671},"sj4cs"," url",[1020,1674,1675],{"class":1667}," =",[1020,1677,1678],{"class":1667}," typeof",[1020,1680,1682],{"class":1681},"sVt8B"," request ",[1020,1684,1685],{"class":1667},"===",[1020,1687,1689],{"class":1688},"sZZnC"," 'string'",[1020,1691,1692],{"class":1667}," ?",[1020,1694,1682],{"class":1681},[1020,1696,503],{"class":1667},[1020,1698,1699],{"class":1681}," (request ",[1020,1701,1702],{"class":1667},"as",[1020,1704,1705],{"class":1681}," { ",[1020,1707,1709],{"class":1708},"s4XuR","url",[1020,1711,1712],{"class":1667},"?:",[1020,1714,1715],{"class":1671}," string",[1020,1717,1718],{"class":1681}," }).url ",[1020,1720,1721],{"class":1667},"??",[1020,1723,1724],{"class":1688}," ''\n",[1020,1726,1727],{"class":1022,"line":1029},[1020,1728,1730],{"emptyLinePlaceholder":1729},true,"\n",[1020,1732,1733],{"class":1022,"line":1035},[1020,1734,1736],{"class":1735},"sJ8bj","\u002F\u002F \u002Fauth\u002F* 라우트의 401은 정상적인 \"잘못된 자격증명·OTP\" → 호출자가 처리해야 함\n",[1020,1738,1740,1743,1746,1750,1753,1756,1759],{"class":1022,"line":1739},4,[1020,1741,1742],{"class":1667},"if",[1020,1744,1745],{"class":1681}," (url.",[1020,1747,1749],{"class":1748},"sScJk","includes",[1020,1751,1752],{"class":1681},"(",[1020,1754,1755],{"class":1688},"'\u002Fauth\u002F'",[1020,1757,1758],{"class":1681},")) ",[1020,1760,1761],{"class":1667},"return\n",[1020,1763,1765],{"class":1022,"line":1764},5,[1020,1766,1730],{"emptyLinePlaceholder":1729},[1020,1768,1770],{"class":1022,"line":1769},6,[1020,1771,1772],{"class":1735},"\u002F\u002F 인증되지 않은 상태에서 보호 라우트 호출 → 의미 있는 리다이렉트 아님\n",[1020,1774,1776,1778,1781,1784,1787,1790],{"class":1022,"line":1775},7,[1020,1777,1742],{"class":1667},[1020,1779,1780],{"class":1681}," (",[1020,1782,1783],{"class":1667},"!",[1020,1785,1786],{"class":1748},"useAuthToken",[1020,1788,1789],{"class":1681},"().value) ",[1020,1791,1761],{"class":1667},[1020,1793,1795],{"class":1022,"line":1794},8,[1020,1796,1730],{"emptyLinePlaceholder":1729},[1020,1798,1800],{"class":1022,"line":1799},9,[1020,1801,1802],{"class":1735},"\u002F\u002F 인증된 상태 + 보호 라우트 401 → 토큰 만료 → \u002Flogin\n",[14,1804,1806],{"id":1805},"_44-회원가입-완료-화면-고객사-id-노출-제거","4.4 회원가입 완료 화면 — 고객사 ID 노출 제거",[19,1808,1809,1810,1813],{},"§2~§3 이후 로그인 시 companyId 외울 필요 없음 → 가입 완료 화면 ",[32,1811,1812],{},"발급된 고객사 ID: {id}"," 라인 제거. 시안 정책상 내부 식별자는 외부 노출하지 않음.",[14,1815,1817],{"id":1816},"_45-토스트-가시성-강화","4.5 토스트 가시성 강화",[238,1819,1820,1834,1840,1854],{},[241,1821,1822,1825,1826,1829,1830,1833],{},[23,1823,1824],{},"위치",": 좌하단 → 오른쪽 위 (",[32,1827,1828],{},"app.vue","의 ",[32,1831,1832],{},"\u003CUApp :toaster=\"{position:'top-right', expand:true, duration:5000}\">"," props로 직접 지정)",[241,1835,1836,1839],{},[23,1837,1838],{},"크기",": 폭 380→440px, 본문 폰트 15→17px, 패딩 16\u002F18→20\u002F24px, 최소 높이 56→68px, 모서리 12px, 그림자 강화, 타이틀 17px\u002F700, 아이콘 26px",[241,1841,1842,1843,1846,1847,54,1850,54,1852,1488],{},"Sonner 표준 셀렉터(",[32,1844,1845],{},"[data-sonner-toast]",") + Nuxt UI 내부 클래스 보강 셀렉터(",[32,1848,1849],{},"> div",[32,1851,19],{},[32,1853,1020],{},[241,1855,1856,1829,1859,1862],{},[32,1857,1858],{},"app.config.ts",[32,1860,1861],{},"ui.toaster"," 설정은 타입(슬롯\u002Fvariant)이 달라 제거, UApp props로 단일화",[14,1864,1866],{"id":1865},"_46-배포-검증","4.6 배포 + 검증",[238,1868,1869,1875],{},[241,1870,1871,1872],{},"Workers #12 Version ",[32,1873,1874],{},"84056c86-09ff-4d2f-a9cc-4c63365fc630",[241,1876,1877,1878,1881,1882,1885,1886,1889,1890,1893,1894,1897],{},"Pages #54(",[32,1879,1880],{},"bf71cd8e",") · #55(",[32,1883,1884],{},"bfd64bcc"," 401 처리) · #56(",[32,1887,1888],{},"eecef0a0"," ID 제거) · #57(",[32,1891,1892],{},"4800d506"," 토스트 1차) · #58(",[32,1895,1896],{},"683c5976"," UApp props) — 누적 5번",[14,1899,1901],{"id":1900},"_47-nhn_mock-secret-임시-적용","4.7 NHN_MOCK secret 임시 적용",[19,1903,1904,1905,1908],{},"라이브 검증 + 실 사용자(",[32,1906,1907],{},"dotype@malgnsoft.com",") 가입을 위해 production에 NHN_MOCK secret을 일시 적용. mockCode가 응답에 노출되어 사용자가 메일 없이도 6자리 코드를 토스트로 확인 가능. 자격증명 등록 시 secret 영구 제거 예정.",[94,1910],{},[10,1912,1914],{"id":1913},"_5-nice-통합인증휴대폰-본인확인-인프라-배포-13-60","§5. NICE 통합인증(휴대폰 본인확인) 인프라 (배포 #13 \u002F #60)",[14,1916,103],{"id":1917},"한-줄-4",[19,1919,1920,1921,1924,1925,1928,1929,1932,1933,1936],{},"§4에서 자체 SMS OTP로 가입 흐름을 통과시켰지만, 이는 \"휴대폰 보유\"만 검증하지 \"본인 확인\"이 아님. 사용자 요청으로 ",[23,1922,1923],{},"NICE 통합인증(M=휴대폰 본인확인)"," 인프라를 통째로 구축. 정본 문서 ",[255,1926,72],{"href":1927},"..\u002FNICE_AUTH"," 신규 작성 → 라이브 DDL 0004 적용(TB_NICE_AUTH + TB_USER에 ci\u002Fbirthdate\u002Fgender\u002Fnational_info\u002Fmobile_co + UNIQUE ci) → NICE 어댑터(mock\u002Freal, AES-256-GCM + PBKDF2 + HMAC) → 3 라우트(init\u002Fcallback\u002Fstatus) → \u002Fauth\u002Fsignup 확장(niceSession 검증·CI 중복 차단·NICE 결과로 이름·휴대폰·생년월일 덮어쓰기) → signup.vue Step 4 통째로 NICE 흐름으로 교체(\"본인 인증하기\" 버튼 + 폴링 + 결과 표시) → NICE_MOCK secret 적용으로 자격증명 발급 전 mock 통과. Workers 배포 #13(Version ",[32,1930,1931],{},"2ab47c1f...","), Pages 배포 #60 (alias ",[32,1934,1935],{},"c9577894","). 라이브 e2e 6 시나리오 통과.",[14,1938,1940],{"id":1939},"_51-결정-사항","5.1 결정 사항",[238,1942,1943,1949,1955,1975,1984],{},[241,1944,1945,1948],{},[23,1946,1947],{},"NICE 통합인증 휴대폰(M)"," 만 1차 — 금융·공동·아이핀(F\u002FU\u002FI)은 후속 확장.",[241,1950,1951,1954],{},[23,1952,1953],{},"자체 SMS OTP는 유지"," — 비밀번호 재설정·이메일 변경 등 단순 검증 영역. 본인 확인은 NICE.",[241,1956,1957,1960,1961,1225,1964,1225,1967,1970,1971,1974],{},[23,1958,1959],{},"mock 모드 우선"," — NICE 자격증명 발급 전이라 외부 호출 없이 동작. 가짜 결과: ",[32,1962,1963],{},"모의 사용자",[32,1965,1966],{},"19900101",[32,1968,1969],{},"01099998888"," \u002F CI는 ",[32,1972,1973],{},"MOCK_CI_\u003CrequestNo>","로 결정적 생성(같은 세션 = 같은 CI → 중복 가입 차단 테스트 가능).",[241,1976,1977,512,1980,1983],{},[23,1978,1979],{},"CI 중복 가입 차단",[32,1981,1982],{},"TB_USER.ci UNIQUE"," + signup 시 명시적 검사. \"이미 가입된 사용자입니다\" 안내 + 비밀번호 재설정 유도.",[241,1985,1986,1989,1990,54,1993,1996,1997,54,1999,2002,2003,54,2006,54,2009,54,2012,54,2015,2018],{},[23,1987,1988],{},"NICE 결과 우선"," — niceSession이 있으면 signup body의 ",[32,1991,1992],{},"name",[32,1994,1995],{},"phone"," 대신 NICE 검증값(",[32,1998,1992],{},[32,2000,2001],{},"mobile_no",") 사용 + ",[32,2004,2005],{},"birthdate",[32,2007,2008],{},"gender",[32,2010,2011],{},"national_info",[32,2013,2014],{},"ci",[32,2016,2017],{},"mobile_co"," 적재.",[14,2020,2022],{"id":2021},"_52-ddl-0004-라이브-적용-완료","5.2 DDL 0004 (라이브 적용 완료)",[19,2024,2025,503],{},[255,2026,2028],{"href":2027},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Fdb\u002Fmigrations\u002F0004_nice_auth.sql","src\u002Fdb\u002Fmigrations\u002F0004_nice_auth.sql",[19,2030,2031,2032,2035],{},"§A ",[32,2033,2034],{},"TB_NICE_AUTH"," 신설 (17 컬럼):",[238,2037,2038,2059,2066,2087,2099],{},[241,2039,2040,2041,2044,2045,2048,2049,122,2052,122,2055,2058],{},"세션 키: ",[32,2042,2043],{},"id"," PK + ",[32,2046,2047],{},"request_no"," UNIQUE + ",[32,2050,2051],{},"transaction_id",[32,2053,2054],{},"ticket",[32,2056,2057],{},"iterators"," (복호화에 필요)",[241,2060,2061,2062,2065],{},"상태: ",[32,2063,2064],{},"state"," (pending\u002Fcompleted\u002Ffailed\u002Fexpired\u002Fconsumed)",[241,2067,2068,2069,2071,2072,2071,2074,2071,2076,2071,2078,2071,2080,2071,2083,2071,2085],{},"결과: ",[32,2070,1992],{},"\u002F",[32,2073,2005],{},[32,2075,2008],{},[32,2077,2011],{},[32,2079,2014],{},[32,2081,2082],{},"di",[32,2084,2017],{},[32,2086,2001],{},[241,2088,2089,2090,2071,2093,2071,2096],{},"시간: ",[32,2091,2092],{},"expires_at",[32,2094,2095],{},"created_at",[32,2097,2098],{},"completed_at",[241,2100,2101,2102,2105,2106],{},"인덱스: ",[32,2103,2104],{},"(state, created_at)"," · ",[32,2107,2108],{},"(ci)",[19,2110,2111,2112,2114],{},"§B ",[32,2113,1058],{}," 5 컬럼 추가:",[238,2116,2117,2134],{},[241,2118,2119,2105,2122,2105,2125,2105,2128,2105,2131],{},[32,2120,2121],{},"birthdate VARCHAR(8)",[32,2123,2124],{},"gender CHAR(1)",[32,2126,2127],{},"national_info CHAR(1)",[32,2129,2130],{},"ci VARCHAR(255)",[32,2132,2133],{},"mobile_co VARCHAR(10)",[241,2135,2136,2139],{},[32,2137,2138],{},"UNIQUE KEY uq_user_ci (ci)"," — 중복 가입 차단",[19,2141,2142,2143,2146,2147,1480,2150,2153],{},"라이브 적용 검증: ",[32,2144,2145],{},"SHOW CREATE TABLE TB_NICE_AUTH"," 정상, ",[32,2148,2149],{},"TB_USER.ci",[32,2151,2152],{},"uq_user_ci"," 인덱스 단독.",[14,2155,2157,2158],{"id":2156},"_53-nice-어댑터-srcadaptersniceauthts","5.3 NICE 어댑터 — ",[32,2159,2160],{},"src\u002Fadapters\u002Fnice\u002Fauth.ts",[238,2162,2163,2176,2193,2209,2218,2231,2244],{},[241,2164,2165,2168,2169,2172,2173,1488],{},[32,2166,2167],{},"requestToken(creds, requestNo)"," → POST ",[32,2170,2171],{},"\u002Fauth\u002Ftoken"," (Basic auth + ",[32,2174,2175],{},"client_credentials",[241,2177,2178,2168,2181,1780,2184,122,2187,122,2190,1488],{},[32,2179,2180],{},"requestAuthUrl(creds, accessToken, requestNo)",[32,2182,2183],{},"\u002Fauth\u002Furl",[32,2185,2186],{},"svc_types: ['M']",[32,2188,2189],{},"return_url",[32,2191,2192],{},"close_url",[241,2194,2195,2168,2198,2201,2202,122,2205,2208],{},[32,2196,2197],{},"requestResult(accessToken, webTxId, txId, requestNo)",[32,2199,2200],{},"\u002Fauth\u002Fresult"," (암호화된 ",[32,2203,2204],{},"enc_data",[32,2206,2207],{},"integrity_value"," 수신)",[241,2210,2211,512,2214,2217],{},[32,2212,2213],{},"deriveKeys(ticket, txId, iters)",[23,2215,2216],{},"PBKDF2-HMAC-SHA256"," 64 bytes 유도 → 대칭키 32 bytes + HMAC키 32 bytes (offset 48)",[241,2219,2220,2223,2224,122,2227,2230],{},[32,2221,2222],{},"decryptResult(raw, ticket, txId, iters)"," — Web Crypto ",[32,2225,2226],{},"crypto.subtle.deriveBits",[32,2228,2229],{},"decrypt({name:'AES-GCM', iv, tagLength:128})"," + HMAC-SHA256 무결성 검증",[241,2232,2233,2236,2237,581,2240,2243],{},[32,2234,2235],{},"mockNiceResult(requestNo)"," — 결정적 가짜 결과 (",[32,2238,2239],{},"name='모의 사용자'",[32,2241,2242],{},"ci='MOCK_CI_\u003CrequestNo>'",", …)",[241,2245,2246],{},"Workers 표준 Web Crypto만 사용 — 외부 라이브러리 0",[14,2248,2250,2251],{"id":2249},"_54-라우트-srcroutesnicets","5.4 라우트 — ",[32,2252,2253],{},"src\u002Froutes\u002Fnice.ts",[143,2255,2256,2266],{},[146,2257,2258],{},[149,2259,2260,2263],{},[152,2261,2262],{},"라우트",[152,2264,2265],{},"동작",[165,2267,2268,2293,2310],{},[149,2269,2270,2275],{},[170,2271,2272],{},[32,2273,2274],{},"POST \u002Fauth\u002Fnice\u002Finit",[170,2276,2277,2278,2281,2282,2285,2286,2289,2290],{},"mock: 즉시 ",[32,2279,2280],{},"completed"," 상태로 가짜 결과 적재 → ",[32,2283,2284],{},"{sessionId, authUrl:null, mockMode:true}",". real: token + url 호출 후 ",[32,2287,2288],{},"pending"," 적재 → ",[32,2291,2292],{},"{sessionId, authUrl, mockMode:false}",[149,2294,2295,2300],{},[170,2296,2297],{},[32,2298,2299],{},"POST \u002Fauth\u002Fnice\u002Fcallback",[170,2301,2302,2303,2306,2307,2309],{},"NICE의 form\u002Fjson ",[32,2304,2305],{},"web_transaction_id"," 수신 → 가장 최근 ",[32,2308,2288],{}," 세션 → result 호출 + 복호화 + DB 업데이트 → HTML 응답(팝업 자동 닫기)",[149,2311,2312,2317],{},[170,2313,2314],{},[32,2315,2316],{},"GET \u002Fauth\u002Fnice\u002Fstatus?session=…",[170,2318,2319,2320,2322],{},"프런트 폴링 — state 조회. ",[32,2321,2280],{},"면 name\u002Fbirthdate\u002Fgender\u002Fnational_info\u002Fmobile_co\u002Fmobile_no 노출 (ci는 서버에서만 보유)",[14,2324,2326,2327,2329],{"id":2325},"_55-authsignup-확장","5.5 ",[32,2328,80],{}," 확장",[19,2331,2332,1480,2335,2338],{},[32,2333,2334],{},"signupB",[32,2336,2337],{},"niceSession?: string"," 추가. 있으면:",[408,2340,2341,2349,2355,2361,2366,2374,2381],{},[241,2342,2343,476,2345,2348],{},[32,2344,2034],{},[32,2346,2347],{},"requestNo = niceSession"," 단건 조회",[241,2350,2351,2354],{},[32,2352,2353],{},"state === 'completed'"," 검증 (consumed\u002Ffailed\u002Fexpired면 401)",[241,2356,2357,2360],{},[32,2358,2359],{},"expires_at > now"," 검증",[241,2362,2363,2365],{},[32,2364,2014],{}," 중복 검사 — 있으면 409 \"이미 가입된 사용자\"",[241,2367,2368,2369,54,2371,2373],{},"signup 시 NICE 결과(",[32,2370,1992],{},[32,2372,2001],{},")로 입력값 덮어쓰기 + birthdate\u002Fgender\u002Fnational_info\u002Fci\u002Fmobile_co 적재",[241,2375,2376,2377,2380],{},"signup 성공 후 ",[32,2378,2379],{},"niceAuth.state = 'consumed'"," 처리 → 재사용 차단",[241,2382,2383,2384,2386],{},"catch 블록의 Duplicate entry 감지 — ",[32,2385,2152],{}," 매치 시 별도 안내",[19,2388,2389],{},"OpenAPI 4지점(2 paths + 4 schemas) + SignupRequest에 niceSession 필드.",[14,2391,2393],{"id":2392},"_56-프런트-signupvue-step-4-통째로-교체","5.6 프런트 signup.vue Step 4 통째로 교체",[19,2395,2396,2397,2400],{},"기존: 통신사 select + 이름 + 주민번호 + 내외국인 + 휴대폰 3분할 + 인증번호 입력 → 6개 필드\n신규: ",[23,2398,2399],{},"\"본인 인증하기\" 큰 버튼 1개"," + 상태 표시",[19,2402,2403,503],{},[32,2404,2405],{},"startNiceAuth()",[408,2407,2408,2415,2426,2433],{},[241,2409,2410,1516,2412],{},[32,2411,2274],{},[32,2413,2414],{},"{sessionId, authUrl, mockMode}",[241,2416,2417,2418,2421,2422,2425],{},"mockMode면 즉시 ",[32,2419,2420],{},"pollNiceStatus()"," 1회 호출 → ",[32,2423,2424],{},"state='completed'"," + 결과 표시",[241,2427,2428,2429,2432],{},"real이면 ",[32,2430,2431],{},"window.open(authUrl, ...)"," + 5초마다 status 폴링 (최대 5분)",[241,2434,2435,2436,122,2439],{},"결과 표시: ",[32,2437,2438],{},"\u003C이름>님 본인 인증이 완료되었습니다. \u003C휴대폰> · \u003C통신사>",[32,2440,2441],{},"verified=true",[19,2443,2444,2447,2448,2451,2452,2455],{},[32,2445,2446],{},"submitSignup()"," 확장: ",[32,2449,2450],{},"niceSession","을 signup body에 전달. NICE 결과의 name·휴대폰을 우선 사용. 409 응답 + ",[32,2453,2454],{},"이미 가입된 사용자"," 메시지 분기.",[19,2457,2458,30,2461,2464,2465,2467],{},[32,2459,2460],{},"stores\u002Fauth.ts",[32,2462,2463],{},"SignupPayload"," 타입에 ",[32,2466,2337],{}," 추가.",[19,2469,2470],{},"기존 입력 필드(통신사·이름·주민번호·휴대폰)와 관련 ref\u002Ffunction들은 다른 곳에서 의존성 없어 UI에서 자연 제거됨(스크립트 ref는 leftover로 남아 있으나 미사용).",[14,2472,2474],{"id":2473},"_57-라이브-e2e-검증-6-시나리오","5.7 라이브 e2e 검증 (6 시나리오)",[143,2476,2477,2487],{},[146,2478,2479],{},[149,2480,2481,2483,2485],{},[152,2482,683],{},[152,2484,686],{},[152,2486,689],{},[165,2488,2489,2503,2517,2543,2556,2565],{},[149,2490,2491,2493,2501],{},[170,2492,696],{},[170,2494,2495,2498,2499],{},[32,2496,2497],{},"\u002Fauth\u002Fnice\u002Finit"," → mock 응답 ",[32,2500,2284],{},[170,2502,702],{},[149,2504,2505,2507,2515],{},[170,2506,707],{},[170,2508,2509,1516,2512],{},[32,2510,2511],{},"\u002Fauth\u002Fnice\u002Fstatus?session=…",[32,2513,2514],{},"{state:'completed', name:'모의 사용자', mobile_no:'01099998888', …}",[170,2516,702],{},[149,2518,2519,2521,2541],{},[170,2520,717],{},[170,2522,2523,2525,2526,54,2528,54,2531,54,2534,54,2537,2540],{},[32,2524,80],{}," with niceSession → 201 + DB에 ",[32,2527,2239],{},[32,2529,2530],{},"birthdate='19900101'",[32,2532,2533],{},"gender='1'",[32,2535,2536],{},"ci='MOCK_CI_…'",[32,2538,2539],{},"mobile_co='SKT'"," 정확 매핑",[170,2542,702],{},[149,2544,2545,2547,2554],{},[170,2546,729],{},[170,2548,2549,2550,2553],{},"같은 niceSession 재사용 → 401 ",[32,2551,2552],{},"NICE 본인 인증이 완료되지 않았습니다"," (consumed)",[170,2555,702],{},[149,2557,2558,2560,2563],{},[170,2559,739],{},[170,2561,2562],{},"새 niceSession (다른 mock CI) → 정상 가입",[170,2564,702],{},[149,2566,2567,2569,2578],{},[170,2568,752],{},[170,2570,2571,2572,581,2575,2577],{},"DB: ",[32,2573,2574],{},"TB_NICE_AUTH.state='consumed'",[32,2576,2149],{}," UNIQUE 정상 동작",[170,2579,702],{},[19,2581,2582],{},"검증 데이터 cleanup 완료 (TB_USER · TB_COMPANY · TB_NICE_AUTH 0건 잔존).",[14,2584,2586,2587],{"id":2585},"_58-정본-문서-docnice_authmd","5.8 정본 문서 — ",[32,2588,72],{},[19,2590,2591],{},"12 섹션 \u002F ~14KB:",[408,2593,2594,2597,2600,2603,2606,2609,2612,2615,2618,2624,2632,2635],{},[241,2595,2596],{},"자체 SMS OTP vs NICE 비교",[241,2598,2599],{},"인증 수단 종류 (M\u002FF\u002FU\u002FI — 우리는 M 우선)",[241,2601,2602],{},"전체 시퀀스 5단계 (ASCII 도식)",[241,2604,2605],{},"엔드포인트 3종",[241,2607,2608],{},"단계별 명세 + JSON 예시",[241,2610,2611],{},"AES-256-GCM + PBKDF2 (Workers Web Crypto)",[241,2613,2614],{},"응답 데이터 (name·birthdate·gender·CI·DI·mobile_co·mobile_no)",[241,2616,2617],{},"우리 적용 계획",[241,2619,2620,2623],{},[23,2621,2622],{},"인프라 고려사항"," — Workers 동적 IP vs NICE 화이트리스트 요구 (협상 또는 자체 프록시 EC2 필요)",[241,2625,2626,2627,2631],{},"NICE 계약 절차 7단계 (1",[2628,2629,2630],"del",{},"4 사용자, 5","7 김도형)",[241,2633,2634],{},"알려진 한계 (외국인·법인 대표자·PASS·CI 중복 검사 등)",[241,2636,2637],{},"다음 단계",[14,2639,2641],{"id":2640},"_59-산출물","5.9 산출물",[238,2643,2644,2673,2694,2700,2706],{},[241,2645,2646,2647,2650,2651,54,2654,54,2657,2660,2661,54,2664,54,2667,54,2670],{},"API: ",[32,2648,2649],{},"malgn-noti-api: b4d8f4b"," — 7 files +922 -11. 신규: ",[32,2652,2653],{},"nice\u002Fauth.ts",[32,2655,2656],{},"routes\u002Fnice.ts",[32,2658,2659],{},"0004_nice_auth.sql",". 수정: ",[32,2662,2663],{},"schema.ts",[32,2665,2666],{},"auth.ts",[32,2668,2669],{},"openapi.ts",[32,2671,2672],{},"index.ts",[241,2674,2675,2676,54,2679,54,2681,54,2684,54,2686,54,2688,2691,2692,1488],{},"사용자단: 5 파일 수정(",[32,2677,2678],{},"signup.vue",[32,2680,2460],{},[32,2682,2683],{},"useApi.ts",[32,2685,1828],{},[32,2687,1858],{},[32,2689,2690],{},"main.css",") + 1 신규(",[32,2693,72],{},[241,2695,2696,2697],{},"Workers 배포 #13 Version ",[32,2698,2699],{},"2ab47c1f-1d68-42d3-815c-117cab3fd71a",[241,2701,2702,2703],{},"Pages 배포 #60 alias ",[32,2704,2705],{},"c9577894.malgn-noti.pages.dev",[241,2707,2708],{},"WBS 5-3C-* 신규 항목: NICE 본인확인 인프라 ✅",[14,2710,2712],{"id":2711},"_510-알려진-한계-다음-단계","5.10 알려진 한계 \u002F 다음 단계",[238,2714,2715,2728,2737,2746,2759,2768],{},[241,2716,2717,2720,2721,122,2724,2727],{},[23,2718,2719],{},"NICE 자격증명 미발급"," — 사용자 영업 작업 선행. 발급 후 ",[32,2722,2723],{},"wrangler secret put NICE_CLIENT_ID\u002FSECRET\u002FRETURN_URL",[32,2725,2726],{},"wrangler secret delete NICE_MOCK","로 real 모드 전환 가능.",[241,2729,2730,512,2733,2736],{},[23,2731,2732],{},"Workers 동적 outbound IP vs NICE 화이트리스트",[255,2734,2735],{"href":1927},"NICE_AUTH.md §9"," 참조. Cloudflare 대역 등록 협상 또는 자체 프록시 EC2 필요. NICE 계약 시점에 결정.",[241,2738,2739,2742,2743,2745],{},[23,2740,2741],{},"콜백 시 세션 매칭"," — 1차 구현은 \"가장 최근 pending 세션\" 휴리스틱. 동시 다중 가입은 드물지만 운영 단계에서 ",[32,2744,2064],{}," 파라미터로 명시화 검토.",[241,2747,2748,512,2751,2754,2755,2758],{},[23,2749,2750],{},"모바일웹 popup 차단",[32,2752,2753],{},"window.open","이 모바일 Safari에서 차단될 수 있음. ",[32,2756,2757],{},"redirect"," 모드 옵션 검토.",[241,2760,2761,512,2764,2767],{},[23,2762,2763],{},"외국인 가입",[32,2765,2766],{},"national_info='1'"," 분기 UI 후속.",[241,2769,2770,2773],{},[23,2771,2772],{},"법인 대표자 본인 인증"," — 정책 결정 후 적용.",[94,2775],{},[10,2777,2779,2780,122,2783,2786],{"id":2778},"_6-accountsettings-실-api-연동-patch-me-patch-mecompany-배포-14-61","§6. \u002Faccount\u002Fsettings 실 API 연동 — ",[32,2781,2782],{},"PATCH \u002Fme",[32,2784,2785],{},"PATCH \u002Fme\u002Fcompany"," (배포 #14 \u002F #61)",[14,2788,103],{"id":2789},"한-줄-5",[19,2791,2792,2793,2795,2796,122,2799,2801,2802,2804,2805,2809,2810,2813,2814,2817,2818,2821,2822,130],{},"WBS 5-3C-7 (PATCH \u002Fme + \u002Faccount\u002Fsettings) 작업. 기존 백엔드 ",[32,2794,249],{},"는 GET만 있었고 응답도 최소(8 필드)였는데, ",[23,2797,2798],{},"GET \u002Fme 응답을 TB_USER 13 + TB_COMPANY 14 컬럼 풀로 확장",[23,2800,2782],{},"(사용자 본인 — name·phone) + ",[23,2803,2785],{},"(회사 — companyPhone·billingEmail·adReceive, owner\u002Fadmin 권한) 신설. 프런트 ",[255,2806,2808],{"href":2807},"..\u002F..\u002Fapp\u002Fcomponents\u002FAppMemberInfoPanel.vue","AppMemberInfoPanel.vue","는 전체 목업 데이터(",[32,2811,2812],{},"account.email='service@malgnsoft.com'"," 등)를 제거하고 ",[32,2815,2816],{},"useAuthStore()"," 기반으로 모두 실 데이터로 교체 — 가입 정보 행은 회사 정보 자동 매핑, 광고성 메일 수신 토글은 즉시 PATCH(컨펌 모달 후), 저장하기는 변경된 필드만 한 번에 PATCH. 라이브 e2e 5건 통과. Workers 배포 #14(Version ",[32,2819,2820],{},"22368d14...","), Pages 배포 #61 (alias ",[32,2823,2824],{},"ea35651d.malgn-noti.pages.dev",[14,2826,2828],{"id":2827},"_61-백엔드-변경","6.1 백엔드 변경",[19,2830,2831,512,2835,2838],{},[255,2832,2834],{"href":2833},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Froutes\u002Fme.ts","src\u002Froutes\u002Fme.ts",[32,2836,2837],{},"readContext()"," 헬퍼로 GET·PATCH 공통 JOIN 쿼리 추출:",[1011,2840,2842],{"className":1658,"code":2841,"language":1660,"meta":1016,"style":1016},"me.use('*', requireAuth())\nme.get('\u002F', ...)                          \u002F\u002F 기존 + 풀 컬럼\nme.patch('\u002F', zValidator(json, patchMeB), ...)        \u002F\u002F name·phone\nme.patch('\u002Fcompany', zValidator(json, patchCompanyB), ...)  \u002F\u002F companyPhone·billingEmail·adReceive\n",[32,2843,2844,2865,2888,2915],{"__ignoreMap":1016},[1020,2845,2846,2849,2852,2854,2857,2859,2862],{"class":1022,"line":1023},[1020,2847,2848],{"class":1681},"me.",[1020,2850,2851],{"class":1748},"use",[1020,2853,1752],{"class":1681},[1020,2855,2856],{"class":1688},"'*'",[1020,2858,581],{"class":1681},[1020,2860,2861],{"class":1748},"requireAuth",[1020,2863,2864],{"class":1681},"())\n",[1020,2866,2867,2869,2872,2874,2877,2879,2882,2885],{"class":1022,"line":1029},[1020,2868,2848],{"class":1681},[1020,2870,2871],{"class":1748},"get",[1020,2873,1752],{"class":1681},[1020,2875,2876],{"class":1688},"'\u002F'",[1020,2878,581],{"class":1681},[1020,2880,2881],{"class":1667},"...",[1020,2883,2884],{"class":1681},")                          ",[1020,2886,2887],{"class":1735},"\u002F\u002F 기존 + 풀 컬럼\n",[1020,2889,2890,2892,2895,2897,2899,2901,2904,2907,2909,2912],{"class":1022,"line":1035},[1020,2891,2848],{"class":1681},[1020,2893,2894],{"class":1748},"patch",[1020,2896,1752],{"class":1681},[1020,2898,2876],{"class":1688},[1020,2900,581],{"class":1681},[1020,2902,2903],{"class":1748},"zValidator",[1020,2905,2906],{"class":1681},"(json, patchMeB), ",[1020,2908,2881],{"class":1667},[1020,2910,2911],{"class":1681},")        ",[1020,2913,2914],{"class":1735},"\u002F\u002F name·phone\n",[1020,2916,2917,2919,2921,2923,2926,2928,2930,2933,2935,2938],{"class":1022,"line":1739},[1020,2918,2848],{"class":1681},[1020,2920,2894],{"class":1748},[1020,2922,1752],{"class":1681},[1020,2924,2925],{"class":1688},"'\u002Fcompany'",[1020,2927,581],{"class":1681},[1020,2929,2903],{"class":1748},[1020,2931,2932],{"class":1681},"(json, patchCompanyB), ",[1020,2934,2881],{"class":1667},[1020,2936,2937],{"class":1681},")  ",[1020,2939,2940],{"class":1735},"\u002F\u002F companyPhone·billingEmail·adReceive\n",[238,2942,2943,2954,2964],{},[241,2944,2945,2946,2949,2950,2953],{},"빈 PATCH(",[32,2947,2948],{},"{}",") → 400 ",[32,2951,2952],{},"validation_failed"," (변경할 필드가 없습니다)",[241,2955,2956,2959,2960,2963],{},[32,2957,2958],{},"\u002Fcompany"," PATCH는 ",[32,2961,2962],{},"role !== 'owner' && role !== 'admin'"," → 403 forbidden",[241,2965,2966,2967,2970],{},"응답은 모두 동일 형식(",[32,2968,2969],{},"{data: {user, company, ctxRole}}",")으로 통일 → 프런트가 변경 후 store 그대로 hydrate 가능",[19,2972,2973,2974,2977,2978,54,2981,2984],{},"OpenAPI: ",[32,2975,2976],{},"Me"," schema 확장 + ",[32,2979,2980],{},"PatchMeRequest",[32,2982,2983],{},"PatchCompanyRequest"," 신규. paths 2 추가.",[14,2986,2988],{"id":2987},"_62-storesauthts-확장","6.2 stores\u002Fauth.ts 확장",[1011,2990,2992],{"className":1658,"code":2991,"language":1660,"meta":1016,"style":1016},"interface AuthUser {  \u002F\u002F +birthdate, gender, nationalInfo, mobileCo, memberType\n  ...\n}\ninterface AuthCompany {  \u002F\u002F +bizNo, bizType, ceoName, upTae, upJong, address, companyPhone, billingEmail, adReceive\n  ...\n}\n\nactions: {\n  async updateMe(patch: {name?, phone?}) { ... }\n  async updateCompany(patch: {companyPhone?, billingEmail?, adReceive?}) { ... }\n}\n",[32,2993,2994,3008,3013,3018,3030,3034,3038,3042,3050,3066,3081],{"__ignoreMap":1016},[1020,2995,2996,2999,3002,3005],{"class":1022,"line":1023},[1020,2997,2998],{"class":1667},"interface",[1020,3000,3001],{"class":1748}," AuthUser",[1020,3003,3004],{"class":1681}," {  ",[1020,3006,3007],{"class":1735},"\u002F\u002F +birthdate, gender, nationalInfo, mobileCo, memberType\n",[1020,3009,3010],{"class":1022,"line":1029},[1020,3011,3012],{"class":1667},"  ...\n",[1020,3014,3015],{"class":1022,"line":1035},[1020,3016,3017],{"class":1681},"}\n",[1020,3019,3020,3022,3025,3027],{"class":1022,"line":1739},[1020,3021,2998],{"class":1667},[1020,3023,3024],{"class":1748}," AuthCompany",[1020,3026,3004],{"class":1681},[1020,3028,3029],{"class":1735},"\u002F\u002F +bizNo, bizType, ceoName, upTae, upJong, address, companyPhone, billingEmail, adReceive\n",[1020,3031,3032],{"class":1022,"line":1764},[1020,3033,3012],{"class":1667},[1020,3035,3036],{"class":1022,"line":1769},[1020,3037,3017],{"class":1681},[1020,3039,3040],{"class":1022,"line":1775},[1020,3041,1730],{"emptyLinePlaceholder":1729},[1020,3043,3044,3047],{"class":1022,"line":1794},[1020,3045,3046],{"class":1748},"actions",[1020,3048,3049],{"class":1681},": {\n",[1020,3051,3052,3055,3058,3061,3063],{"class":1022,"line":1799},[1020,3053,3054],{"class":1681},"  async ",[1020,3056,3057],{"class":1748},"updateMe",[1020,3059,3060],{"class":1681},"(patch: {name?, phone?}) { ",[1020,3062,2881],{"class":1667},[1020,3064,3065],{"class":1681}," }\n",[1020,3067,3069,3071,3074,3077,3079],{"class":1022,"line":3068},10,[1020,3070,3054],{"class":1681},[1020,3072,3073],{"class":1748},"updateCompany",[1020,3075,3076],{"class":1681},"(patch: {companyPhone?, billingEmail?, adReceive?}) { ",[1020,3078,2881],{"class":1667},[1020,3080,3065],{"class":1681},[1020,3082,3084],{"class":1022,"line":3083},11,[1020,3085,3017],{"class":1681},[14,3087,3089],{"id":3088},"_63-appmemberinfopanelvue-전면-교체","6.3 AppMemberInfoPanel.vue 전면 교체",[143,3091,3092,3105],{},[146,3093,3094],{},[149,3095,3096,3099,3102],{},[152,3097,3098],{},"영역",[152,3100,3101],{},"기존 (목업)",[152,3103,3104],{},"신규 (실 데이터)",[165,3106,3107,3126,3147,3161,3176,3192,3206,3223,3237,3248,3259,3270],{},[149,3108,3109,3112,3118],{},[170,3110,3111],{},"데이터 로드",[170,3113,3114,3115],{},"하드코딩 ",[32,3116,3117],{},"account = {email: 'service@malgnsoft.com', ...}",[170,3119,3120,122,3123],{},[32,3121,3122],{},"onMounted(auth.fetchMe)",[32,3124,3125],{},"computed u\u002Fc",[149,3127,3128,3131,3137],{},[170,3129,3130],{},"가입 정보 8행",[170,3132,3133,3136],{},[32,3134,3135],{},"INFO_ROWS"," 고정",[170,3138,3139,3142,3143,3146],{},[32,3140,3141],{},"c.value","의 bizNo\u002FbizType\u002FceoName\u002FupTae\u002FupJong\u002Faddress 자동 매핑, ",[32,3144,3145],{},"BIZ_TYPE_LABEL","로 한국어 표시",[149,3148,3149,3152,3155],{},[170,3150,3151],{},"사업자등록증 변경 버튼",[170,3153,3154],{},"항상 노출",[170,3156,3157,3160],{},[32,3158,3159],{},"c.bizType !== 'personal'"," 일 때만 (개인 유형은 노출 X)",[149,3162,3163,3166,3169],{},[170,3164,3165],{},"광고성 메일 수신 토글",[170,3167,3168],{},"로컬 ref 토글 + 토스트만",[170,3170,3171,3172,3175],{},"컨펌 모달 → ",[32,3173,3174],{},"auth.updateCompany({adReceive})"," 즉시 호출 + 토스트",[149,3177,3178,3181,3186],{},[170,3179,3180],{},"서비스 담당자 이름",[170,3182,3114,3183],{},[32,3184,3185],{},"'홍길동'",[170,3187,3188,3191],{},[32,3189,3190],{},"u.value.name"," (NICE 검증 결과)",[149,3193,3194,3197,3200],{},[170,3195,3196],{},"회사 전화번호 입력",[170,3198,3199],{},"로컬 ref",[170,3201,3202,3205],{},[32,3203,3204],{},"companyPhoneInput"," ref, watchEffect로 store에서 초기화",[149,3207,3208,3211,3213],{},[170,3209,3210],{},"휴대전화번호 3분할",[170,3212,3199],{},[170,3214,3215,3218,3219,3222],{},[32,3216,3217],{},"watchEffect","가 ",[32,3220,3221],{},"u.value.phone","에서 010\u002F3~4자리\u002F4자리로 자동 split",[149,3224,3225,3228,3231],{},[170,3226,3227],{},"결제 이메일 변경",[170,3229,3230],{},"로컬 ref 변경 + 토스트",[170,3232,3233,3234],{},"다이얼로그 → ",[32,3235,3236],{},"auth.updateCompany({billingEmail})",[149,3238,3239,3242,3245],{},[170,3240,3241],{},"서비스 담당자 이메일 변경",[170,3243,3244],{},"로컬 ref 변경",[170,3246,3247],{},"\"곧 지원됩니다\" 안내 — OTP 검증 흐름은 후속",[149,3249,3250,3253,3256],{},[170,3251,3252],{},"휴대폰 본인 인증",[170,3254,3255],{},"NICE 미연결 더미",[170,3257,3258],{},"그대로 더미 — NICE Step 4와 별개로 후속",[149,3260,3261,3264,3267],{},[170,3262,3263],{},"회원 탈퇴",[170,3265,3266],{},"로컬 토스트",[170,3268,3269],{},"\"곧 지원됩니다\" 안내 — 후속 라우트 필요",[149,3271,3272,3277,3280],{},[170,3273,3274],{},[23,3275,3276],{},"저장하기",[170,3278,3279],{},"토스트만",[170,3281,3282,54,3285,3288,3289,54,3291,3293],{},[32,3283,3284],{},"companyPhone",[32,3286,3287],{},"fullPhone"," 변경 감지 → ",[32,3290,3057],{},[32,3292,3073],{}," 병렬 호출",[19,3295,3296],{},"저장 로직:",[1011,3298,3300],{"className":1658,"code":3299,"language":1660,"meta":1016,"style":1016},"const tasks = []\nif (fullPhone !== u.phone) tasks.push(updateMe({phone: fullPhone}))\nif (companyPhoneInput !== c.companyPhone) tasks.push(updateCompany({companyPhone: companyPhoneInput}))\nif (tasks.length === 0) toast(변경 없음)\nelse await Promise.all(tasks) → 성공 토스트\n",[32,3301,3302,3314,3337,3358,3383],{"__ignoreMap":1016},[1020,3303,3304,3306,3309,3311],{"class":1022,"line":1023},[1020,3305,1668],{"class":1667},[1020,3307,3308],{"class":1671}," tasks",[1020,3310,1675],{"class":1667},[1020,3312,3313],{"class":1681}," []\n",[1020,3315,3316,3318,3321,3324,3327,3330,3332,3334],{"class":1022,"line":1029},[1020,3317,1742],{"class":1667},[1020,3319,3320],{"class":1681}," (fullPhone ",[1020,3322,3323],{"class":1667},"!==",[1020,3325,3326],{"class":1681}," u.phone) tasks.",[1020,3328,3329],{"class":1748},"push",[1020,3331,1752],{"class":1681},[1020,3333,3057],{"class":1748},[1020,3335,3336],{"class":1681},"({phone: fullPhone}))\n",[1020,3338,3339,3341,3344,3346,3349,3351,3353,3355],{"class":1022,"line":1035},[1020,3340,1742],{"class":1667},[1020,3342,3343],{"class":1681}," (companyPhoneInput ",[1020,3345,3323],{"class":1667},[1020,3347,3348],{"class":1681}," c.companyPhone) tasks.",[1020,3350,3329],{"class":1748},[1020,3352,1752],{"class":1681},[1020,3354,3073],{"class":1748},[1020,3356,3357],{"class":1681},"({companyPhone: companyPhoneInput}))\n",[1020,3359,3360,3362,3365,3368,3371,3374,3377,3380],{"class":1022,"line":1739},[1020,3361,1742],{"class":1667},[1020,3363,3364],{"class":1681}," (tasks.",[1020,3366,3367],{"class":1671},"length",[1020,3369,3370],{"class":1667}," ===",[1020,3372,3373],{"class":1671}," 0",[1020,3375,3376],{"class":1681},") ",[1020,3378,3379],{"class":1748},"toast",[1020,3381,3382],{"class":1681},"(변경 없음)\n",[1020,3384,3385,3388,3391,3394,3396,3399],{"class":1022,"line":1764},[1020,3386,3387],{"class":1667},"else",[1020,3389,3390],{"class":1667}," await",[1020,3392,3393],{"class":1671}," Promise",[1020,3395,806],{"class":1681},[1020,3397,3398],{"class":1748},"all",[1020,3400,3401],{"class":1681},"(tasks) → 성공 토스트\n",[19,3403,3404,3407],{},[32,3405,3406],{},".seg"," 스타일도 추가 (광고성 메일 수신 라디오 토글 — 기존 누락).",[14,3409,3411],{"id":3410},"_64-라이브-e2e-production","6.4 라이브 e2e (Production)",[143,3413,3414,3425],{},[146,3415,3416],{},[149,3417,3418,3420,3423],{},[152,3419,683],{},[152,3421,3422],{},"호출",[152,3424,689],{},[165,3426,3427,3440,3452,3464,3476],{},[149,3428,3429,3431,3437],{},[170,3430,696],{},[170,3432,3433,3436],{},[32,3434,3435],{},"GET \u002Fme"," (Bearer)",[170,3438,3439],{},"200 + 풀 컨텍스트 (TB_USER 13 + TB_COMPANY 14 컬럼)",[149,3441,3442,3444,3449],{},[170,3443,707],{},[170,3445,3446],{},[32,3447,3448],{},"PATCH \u002Fme {name:'김도형', phone:'010-1111-2222'}",[170,3450,3451],{},"200 + 갱신된 user 응답",[149,3453,3454,3456,3461],{},[170,3455,717],{},[170,3457,3458],{},[32,3459,3460],{},"PATCH \u002Fme\u002Fcompany {companyPhone, billingEmail, adReceive:'reject'}",[170,3462,3463],{},"200 + 갱신된 company 응답",[149,3465,3466,3468,3473],{},[170,3467,729],{},[170,3469,3470,3472],{},[32,3471,3435],{}," 재호출",[170,3474,3475],{},"name\u002Fphone\u002FcompanyPhone\u002FbillingEmail\u002FadReceive 모두 정확 반영",[149,3477,3478,3480,3485],{},[170,3479,739],{},[170,3481,3482,3483],{},"빈 PATCH ",[32,3484,2948],{},[170,3486,3487,3488,3490],{},"400 ",[32,3489,2952],{},": 변경할 필드가 없습니다",[19,3492,3493,3494,3497],{},"테스트 사용자(",[32,3495,3496],{},"mep-test-…",") cleanup 완료.",[14,3499,3501],{"id":3500},"_65-산출물","6.5 산출물",[238,3503,3504,3518,3530],{},[241,3505,2646,3506,512,3509,3511,3512,3514,3515],{},[32,3507,3508],{},"malgn-noti-api: c8c…",[32,3510,2834],{}," 전면 개편(+150) · ",[32,3513,779],{}," 갱신. Workers 배포 #14 Version ",[32,3516,3517],{},"22368d14-f0c8-4788-8b52-5cb4f6442cf3",[241,3519,3520,3521,3523,3524,3527,3528],{},"사용자단: ",[32,3522,790],{}," 타입 확장 + 2 액션 \u002F ",[32,3525,3526],{},"app\u002Fcomponents\u002FAppMemberInfoPanel.vue"," 전면 교체. Pages 배포 #61 alias ",[32,3529,2824],{},[241,3531,3532],{},"WBS 5-3C-7 ⚪ → 🟢 (회원 정보 변경 — 저장하기·광고수신 즉시 변경·결제이메일 변경은 실 API, 서비스 담당자 이메일·휴대폰 본인 인증 변경은 후속)",[14,3534,3536],{"id":3535},"_66-알려진-한계-후속-작업","6.6 알려진 한계 \u002F 후속 작업",[238,3538,3539,3548,3554,3569],{},[241,3540,3541,3543,3544,3547],{},[23,3542,3241],{}," — OTP 검증 흐름 필요. 백엔드 ",[32,3545,3546],{},"POST \u002Fme\u002Femail-change\u002F{request,confirm}"," 신설 후 다이얼로그 연결.",[241,3549,3550,3553],{},[23,3551,3552],{},"휴대폰 본인 인증 변경"," — NICE 재인증 흐름 또는 SMS OTP. signup의 NICE Step 4와 유사한 패턴 재사용 가능.",[241,3555,3556,512,3558,520,3561,3564,3565,3568],{},[23,3557,3263],{},[32,3559,3560],{},"DELETE \u002Fme",[32,3562,3563],{},"POST \u002Fme\u002Fwithdraw"," 신설 + soft-delete (",[32,3566,3567],{},"TB_USER.status = -1",") + 관련 데이터 정책 결정.",[241,3570,3571,3577],{},[23,3572,3573,3576],{},[32,3574,3575],{},"canEditCompany"," 권한 UX"," — 현재는 PATCH 호출 후 403 에러로 안내. 사전에 role 기반으로 UI 비활성화 검토.",[94,3579],{},[10,3581,3583],{"id":3582},"_7-사업자등록증-심사-승인-게이트-정책-정합화-배포-15-64","§7. 사업자등록증 심사 승인 게이트 — 정책 정합화 (배포 #15 \u002F #64)",[14,3585,103],{"id":3586},"한-줄-6",[19,3588,3589,3590,581,3593,3596,3597,3600,3601,3604,3605,54,3607,3609,3610,3613,3614,3617],{},"새 정책: ",[23,3591,3592],{},"법인 사업자(corp) \u002F 개인 사업자(sole)는 가입 후 사업자등록증 심사 승인을 받아야 서비스 이용 및 가입 정보 수정 가능",[23,3594,3595],{},"개인(personal)은 즉시 사용 가능",". 그동안은 모든 가입자가 ",[32,3598,3599],{},"joinState='joined'"," 즉시 통과였는데, ",[32,3602,3603],{},"TB_COMPANY.approval_state"," 컬럼 + signup 자동 분기 + ",[32,3606,2782],{},[32,3608,2785],{}," 차단 + 프런트 배너·입력 disabled + 가입 완료 화면 분기로 인프라화. 0005 라이브 적용, 기존 5개 회사는 'approved' 기본값으로 호환성 유지. Workers 배포 #15(Version ",[32,3611,3612],{},"6e47d50b...","), Pages 배포 #64 (alias ",[32,3615,3616],{},"56e94e5b.malgn-noti.pages.dev","). 라이브 e2e 8 시나리오 통과(법인 가입 pending \u002F 수정 시도 403 \u002F 개인 가입 approved \u002F 개인 수정 통과 \u002F 운영자 승인 후 수정 통과 \u002F 반려 시뮬레이션 → 사유 노출 403 …).",[14,3619,3621],{"id":3620},"_71-정책-사용자-결정","7.1 정책 (사용자 결정)",[143,3623,3624,3640],{},[146,3625,3626],{},[149,3627,3628,3631,3634,3637],{},[152,3629,3630],{},"회사 유형",[152,3632,3633],{},"가입 직후 상태",[152,3635,3636],{},"서비스 이용",[152,3638,3639],{},"정보 수정",[165,3641,3642,3660,3676],{},[149,3643,3644,3650,3655,3658],{},[170,3645,3646,3649],{},[32,3647,3648],{},"corp"," 법인사업자",[170,3651,3652],{},[32,3653,3654],{},"approval_state='pending'",[170,3656,3657],{},"❌",[170,3659,3657],{},[149,3661,3662,3668,3672,3674],{},[170,3663,3664,3667],{},[32,3665,3666],{},"sole"," 개인사업자",[170,3669,3670],{},[32,3671,3654],{},[170,3673,3657],{},[170,3675,3657],{},[149,3677,3678,3684,3689,3691],{},[170,3679,3680,3683],{},[32,3681,3682],{},"personal"," 개인",[170,3685,3686],{},[32,3687,3688],{},"approval_state='approved'",[170,3690,702],{},[170,3692,702],{},[19,3694,3695,3696,520,3702,3709],{},"운영자가 BackOffice에서 사업자등록증을 심사 → ",[23,3697,3698,3699,1488],{},"승인(",[32,3700,3701],{},"approved",[23,3703,3704,3705,3708],{},"반려(",[32,3706,3707],{},"rejected"," + 사유)"," 처리. 1차에서는 운영자 화면 미구현이라 라이브 DB 직접 UPDATE로 검증(후속에서 운영자단 화면 신설 예정).",[14,3711,3713],{"id":3712},"_72-ddl-0005-라이브-적용-완료","7.2 DDL 0005 (라이브 적용 완료)",[19,3715,3716,503],{},[255,3717,3719],{"href":3718},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Fdb\u002Fmigrations\u002F0005_company_approval.sql","src\u002Fdb\u002Fmigrations\u002F0005_company_approval.sql",[1011,3721,3723],{"className":1013,"code":3722,"language":1015,"meta":1016,"style":1016},"ALTER TABLE TB_COMPANY\n  ADD COLUMN company_type    VARCHAR(20) NULL  COMMENT 'corp\u002Fsole\u002Fpersonal' AFTER name,\n  ADD COLUMN approval_state  VARCHAR(20) NOT NULL DEFAULT 'approved'        AFTER company_type,\n  ADD COLUMN rejected_reason VARCHAR(255) NULL                              AFTER approval_state,\n  ADD KEY idx_company_approval (approval_state, created_at);\n",[32,3724,3725,3730,3735,3740,3745],{"__ignoreMap":1016},[1020,3726,3727],{"class":1022,"line":1023},[1020,3728,3729],{},"ALTER TABLE TB_COMPANY\n",[1020,3731,3732],{"class":1022,"line":1029},[1020,3733,3734],{},"  ADD COLUMN company_type    VARCHAR(20) NULL  COMMENT 'corp\u002Fsole\u002Fpersonal' AFTER name,\n",[1020,3736,3737],{"class":1022,"line":1035},[1020,3738,3739],{},"  ADD COLUMN approval_state  VARCHAR(20) NOT NULL DEFAULT 'approved'        AFTER company_type,\n",[1020,3741,3742],{"class":1022,"line":1739},[1020,3743,3744],{},"  ADD COLUMN rejected_reason VARCHAR(255) NULL                              AFTER approval_state,\n",[1020,3746,3747],{"class":1022,"line":1764},[1020,3748,3749],{},"  ADD KEY idx_company_approval (approval_state, created_at);\n",[19,3751,3752,3753,3755],{},"기존 5행 모두 자동으로 ",[32,3754,3701],{}," — 운영 데이터 호환성 유지.",[14,3757,3759],{"id":3758},"_73-백엔드-변경","7.3 백엔드 변경",[1040,3761,3763],{"id":3762},"signup-확장","signup 확장",[238,3765,3766,3774,3785],{},[241,3767,3768,1480,3770,3773],{},[32,3769,2334],{},[32,3771,3772],{},"companyType: enum(corp\u002Fsole\u002Fpersonal).optional()"," 추가",[241,3775,3776,3777,3780,3781,3784],{},"사업자(corp\u002Fsole) → ",[32,3778,3779],{},"approvalState='pending'",", 그 외 → ",[32,3782,3783],{},"'approved'"," 자동 분기",[241,3786,3787,3788,54,3790,3793],{},"회사 row INSERT 시 ",[32,3789,444],{},[32,3791,3792],{},"approvalState"," 함께 적재",[1040,3795,3797],{"id":3796},"me-응답에-승인-정보-노출","\u002Fme 응답에 승인 정보 노출",[238,3799,3800],{},[241,3801,3802,3803,3806,3807,2105,3809,2105,3811,3814],{},"GET \u002F PATCH 응답의 ",[32,3804,3805],{},"company"," 객체에 ",[32,3808,444],{},[32,3810,3792],{},[32,3812,3813],{},"rejectedReason"," 추가 (3 군데 응답 빌더 모두)",[1040,3816,3818],{"id":3817},"patch-차단","PATCH 차단",[238,3820,3821,3847],{},[241,3822,3823,54,3825,3827,3828,3830,3831,3834,3835],{},[32,3824,2782],{},[32,3826,2785],{}," 둘 다 핸들러 시작부에서 ",[32,3829,2837],{}," 호출 → ",[32,3832,3833],{},"approvalState !== 'approved'","면 403 + 상황별 메시지:\n",[238,3836,3837,3842],{},[241,3838,3839,3841],{},[32,3840,2288],{}," → \"사업자등록증 심사 승인 후 정보를 수정할 수 있습니다.\"",[241,3843,3844,3846],{},[32,3845,3707],{}," → \"심사가 반려되어 정보를 수정할 수 없습니다. 사유: …\"",[241,3848,3849,3850,3853],{},"발송·이력 등 다른 도메인 라우트 차단은 후속 (별도 미들웨어 ",[32,3851,3852],{},"requireApproved()","로 일관화 검토)",[14,3855,3857],{"id":3856},"_74-사용자단-변경","7.4 사용자단 변경",[1040,3859,2460],{"id":3860},"storesauthts",[238,3862,3863,3877],{},[241,3864,3865,1480,3868,2105,3871,2105,3874,3773],{},[32,3866,3867],{},"AuthCompany",[32,3869,3870],{},"companyType?",[32,3872,3873],{},"approvalState?",[32,3875,3876],{},"rejectedReason?",[241,3878,3879,1480,3881,3773],{},[32,3880,2463],{},[32,3882,3870],{},[1040,3884,2678],{"id":3885},"signupvue",[238,3887,3888,3898],{},[241,3889,3890,3893,3894,3897],{},[32,3891,3892],{},"auth.signup({...})"," 호출 시 ",[32,3895,3896],{},"companyType: userType.value || undefined"," 전달",[241,3899,3900,3901],{},"Step 5(가입 완료) 화면 분기:\n",[238,3902,3903,3906],{},[241,3904,3905],{},"사업자: \"사업자등록증 심사가 진행됩니다. 승인 완료 전에는 서비스 이용 및 정보 수정이 제한되며, 결과는 등록하신 휴대폰·이메일로 안내됩니다.\"",[241,3907,3908],{},"개인: \"지금부터 바로 서비스를 이용하실 수 있습니다.\"",[1040,3910,2808],{"id":3911},"appmemberinfopanelvue",[238,3913,3914,3921,3929,3938,3946],{},[241,3915,3916,3917,3920],{},"상단 ",[23,3918,3919],{},"승인 상태 배너"," (pending=warning, rejected=danger). pending이면 \"사업자등록증 심사 중입니다 — 승인 완료 전까지 서비스 이용 및 회원 정보 수정이 제한됩니다.\" rejected면 반려 사유 + \"사업자등록증을 다시 제출해 주세요.\"",[241,3922,3923,3926,3927,1488],{},[32,3924,3925],{},"isLocked"," computed (",[32,3928,3833],{},[241,3930,3931,3932],{},"광고성 메일 수신 토글 2개 · 회사 전화번호 입력 · 휴대전화 select+input 2개 · 이메일 변경 버튼 2개(서비스 담당자·결제) · 휴대폰 인증 버튼 · 저장하기 버튼 — ",[23,3933,3934,3935],{},"모두 ",[32,3936,3937],{},":disabled=\"isLocked\"",[241,3939,3940,1516,3942,3945],{},[32,3941,3159],{},[32,3943,3944],{},"c.companyType !== 'personal'","로 조건 수정 (이전엔 bizType 사용)",[241,3947,3948],{},"배너 스타일: 좌측 24px 아이콘 + 우측 굵은 헤더 + 본문, warning\u002Fdanger 색상 변형",[14,3950,3952],{"id":3951},"_75-라이브-e2e-검증-8-시나리오","7.5 라이브 e2e 검증 (8 시나리오)",[143,3954,3955,3965],{},[146,3956,3957],{},[149,3958,3959,3961,3963],{},[152,3960,683],{},[152,3962,686],{},[152,3964,689],{},[165,3966,3967,3981,3993,4005,4019,4031,4043,4056],{},[149,3968,3969,3971,3979],{},[170,3970,696],{},[170,3972,3973,3974,1516,3976],{},"법인 가입 → ",[32,3975,249],{},[32,3977,3978],{},"companyType='corp', approvalState='pending'",[170,3980,702],{},[149,3982,3983,3985,3991],{},[170,3984,707],{},[170,3986,3987,3990],{},[32,3988,3989],{},"PATCH \u002Fme {name}"," → 403 + \"사업자등록증 심사 승인 후 …\"",[170,3992,702],{},[149,3994,3995,3997,4003],{},[170,3996,717],{},[170,3998,3999,4002],{},[32,4000,4001],{},"PATCH \u002Fme\u002Fcompany {adReceive}"," → 403 + 동일 메시지",[170,4004,702],{},[149,4006,4007,4009,4017],{},[170,4008,729],{},[170,4010,4011,4012,1516,4014],{},"개인 가입 → ",[32,4013,249],{},[32,4015,4016],{},"companyType='personal', approvalState='approved'",[170,4018,702],{},[149,4020,4021,4023,4029],{},[170,4022,739],{},[170,4024,4025,4026,4028],{},"개인 ",[32,4027,3989],{}," → 200 + 변경 반영",[170,4030,702],{},[149,4032,4033,4035,4041],{},[170,4034,752],{},[170,4036,4037,4038,4040],{},"운영자 DB 직접 UPDATE → ",[32,4039,3688],{}," (BackOffice 승인 시뮬레이션)",[170,4042,702],{},[149,4044,4045,4048,4054],{},[170,4046,4047],{},"7",[170,4049,4050,4051,4053],{},"승인 후 법인 ",[32,4052,2782],{}," 재시도 → 200 + 변경 반영",[170,4055,702],{},[149,4057,4058,4061,4068],{},[170,4059,4060],{},"8",[170,4062,4063,4064,4067],{},"반려 시뮬레이션 (",[32,4065,4066],{},"approval_state='rejected', rejected_reason='…'",") → PATCH 시도 → 403 + 사유 메시지 포함",[170,4069,702],{},[19,4071,4072],{},"검증 데이터(법인-…·개인-… 4건) cleanup 완료.",[14,4074,4076],{"id":4075},"_76-산출물","7.6 산출물",[238,4078,4079,4101,4114],{},[241,4080,2646,4081,512,4084,4087,4088,4090,4091,4093,4094,4097,4098],{},[32,4082,4083],{},"malgn-noti-api: 7…",[32,4085,4086],{},"0005_company_approval.sql"," 신규 · ",[32,4089,2663],{}," company 확장 · ",[32,4092,2666],{}," signup 분기 · ",[32,4095,4096],{},"me.ts"," 응답+차단. Workers 배포 #15 Version ",[32,4099,4100],{},"6e47d50b-0225-41d9-8bc8-598045659df8",[241,4102,3520,4103,4105,4106,4108,4109,4111,4112],{},[32,4104,2460],{}," 타입 · ",[32,4107,2678],{}," companyType 전달 + Step 5 분기 · ",[32,4110,2808],{}," 배너 + isLocked + 모든 입력 disabled. Pages 배포 #64 alias ",[32,4113,3616],{},[241,4115,4116,4117,4119],{},"WBS 갱신: 5-3C-6(",[32,4118,444],{}," 전달·저장 + 개인 유형 화면 분기) ⚪→🟢 + 새 항목 5-3C-17(승인 게이트) ✅",[14,4121,4123],{"id":4122},"_77-알려진-한계-후속-작업","7.7 알려진 한계 \u002F 후속 작업",[238,4125,4126,4136,4152,4167,4176,4184],{},[241,4127,4128,4131,4132,4135],{},[23,4129,4130],{},"운영자단 승인 화면 미구현"," — 현재 라이브 DB 직접 UPDATE로만 승인\u002F반려 가능. 운영자단(",[32,4133,4134],{},"\u002Fadmin\u002Fmember\u002Fcompany\u002F[id]",")에 승인·반려(사유 입력) UI 신설 필요. WBS 5-4-3.",[241,4137,4138,4141,4142,4144,4145,4148,4149,4151],{},[23,4139,4140],{},"발송·이력 등 다른 도메인 라우트 차단"," — 현재는 ",[32,4143,249],{}," PATCH만 차단. 발송(",[32,4146,4147],{},"POST \u002Fsend\u002F*","), 캠페인, 발신정보 변경 등도 미승인 차단 필요. ",[32,4150,3852],{}," 미들웨어로 일관화 후 적용 권장. 후속.",[241,4153,4154,512,4157,581,4160,581,4163,4166],{},[23,4155,4156],{},"사용자단 다른 화면 disabled",[32,4158,4159],{},"\u002Faccount\u002Fcards",[32,4161,4162],{},"\u002Fcharge",[32,4164,4165],{},"\u002Fsend\u002F*"," 등도 isLocked일 때 차단\u002F안내 필요. 화면별 점검 후속.",[241,4168,4169,4141,4172,4175],{},[23,4170,4171],{},"GNB·홈 글로벌 안내",[32,4173,4174],{},"\u002Faccount\u002Fsettings","에만 배너. 모든 화면 상단(GNB)에 글로벌 안내 띠 검토.",[241,4177,4178,4183],{},[23,4179,4180,4182],{},[32,4181,3852],{}," 미들웨어 추출"," — 현재는 핸들러 내부 인라인. 도메인 라우트 전부 적용 시점에 별도 헬퍼로 분리.",[241,4185,4186,4189],{},[23,4187,4188],{},"이메일·SMS 자동 안내"," — 승인\u002F반려 처리 시 사용자에게 자동 발송. NHN 자격증명 등록 후 trigger.",[94,4191],{},[10,4193,4195,4196,4198],{"id":4194},"_8-승인-게이트-전-도메인-일관-적용-requireapproved-미들웨어-배포-16","§8. 승인 게이트 전 도메인 일관 적용 — ",[32,4197,3852],{}," 미들웨어 (배포 #16)",[14,4200,103],{"id":4201},"한-줄-7",[19,4203,4204,4205,54,4207,4209,4210,4216,4217,4220,4221,4224,4225,4228,4229,4232],{},"§7에서 ",[32,4206,2782],{},[32,4208,2785],{},"에만 인라인 차단했던 승인 게이트를 ",[23,4211,4212,4213,4215],{},"공용 미들웨어 ",[32,4214,3852],{},"로 추출","하고 ",[23,4218,4219],{},"18개 도메인 라우트에 일괄 적용",". 정책은 ",[32,4222,4223],{},"mutate-only"," — POST\u002FPATCH\u002FPUT\u002FDELETE만 차단(GET 조회는 통과). ",[32,4226,4227],{},"\u002Finquiries","만 예외 — 승인 관련 문의는 미승인 상태에서도 작성 가능. 자동화 스크립트로 18 라우트의 import + use 라인을 일관 갱신, typecheck 통과, Workers 배포 #16(Version ",[32,4230,4231],{},"798bf6f5-bac2-4912-abdd-4af9718c1a93","). 라이브 e2e 6 시나리오 통과(GET 통과 \u002F POST 4건 403 \u002F 개인 가입은 정상 생성 \u002F 문의 작성은 차단 안 됨).",[14,4234,4236,4237],{"id":4235},"_81-미들웨어-srcmiddlewareapprovalts","8.1 미들웨어 — ",[32,4238,4239],{},"src\u002Fmiddleware\u002Fapproval.ts",[19,4241,4242,4245],{},[255,4243,4239],{"href":4244},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Fmiddleware\u002Fapproval.ts"," 신규:",[1011,4247,4249],{"className":1658,"code":4248,"language":1660,"meta":1016,"style":1016},"const MUTATE_METHODS = new Set(['POST', 'PATCH', 'PUT', 'DELETE'])\n\nexport function requireApproved(opts: { method?: 'mutate-only' | 'all' } = {}): MiddlewareHandler\u003CAuthEnv> {\n  const mode = opts.method ?? 'mutate-only'\n  return async (c, next) => {\n    if (mode === 'mutate-only' && !MUTATE_METHODS.has(c.req.method)) return next()\n    const { companyId } = authCtx(c)\n    const db = await getDb(c.env, c.executionCtx)\n    const rows = await db.select({...}).from(company).where(eq(company.id, companyId)).limit(1)\n    const row = rows[0]\n    if (!row) throw errors.notFound('company')\n    if (row.approvalState !== 'approved') {\n      throw errors.forbidden(\u002F* pending \u002F rejected 별 메시지 *\u002F)\n    }\n    await next()\n  }\n}\n",[32,4250,4251,4290,4294,4351,4369,4395,4433,4452,4469,4521,4539,4566,4582,4600,4606,4616,4622],{"__ignoreMap":1016},[1020,4252,4253,4255,4258,4260,4263,4266,4269,4272,4274,4277,4279,4282,4284,4287],{"class":1022,"line":1023},[1020,4254,1668],{"class":1667},[1020,4256,4257],{"class":1671}," MUTATE_METHODS",[1020,4259,1675],{"class":1667},[1020,4261,4262],{"class":1667}," new",[1020,4264,4265],{"class":1748}," Set",[1020,4267,4268],{"class":1681},"([",[1020,4270,4271],{"class":1688},"'POST'",[1020,4273,581],{"class":1681},[1020,4275,4276],{"class":1688},"'PATCH'",[1020,4278,581],{"class":1681},[1020,4280,4281],{"class":1688},"'PUT'",[1020,4283,581],{"class":1681},[1020,4285,4286],{"class":1688},"'DELETE'",[1020,4288,4289],{"class":1681},"])\n",[1020,4291,4292],{"class":1022,"line":1029},[1020,4293,1730],{"emptyLinePlaceholder":1729},[1020,4295,4296,4299,4302,4305,4307,4310,4312,4314,4317,4319,4322,4325,4328,4331,4334,4337,4339,4342,4345,4348],{"class":1022,"line":1035},[1020,4297,4298],{"class":1667},"export",[1020,4300,4301],{"class":1667}," function",[1020,4303,4304],{"class":1748}," requireApproved",[1020,4306,1752],{"class":1681},[1020,4308,4309],{"class":1708},"opts",[1020,4311,503],{"class":1667},[1020,4313,1705],{"class":1681},[1020,4315,4316],{"class":1708},"method",[1020,4318,1712],{"class":1667},[1020,4320,4321],{"class":1688}," 'mutate-only'",[1020,4323,4324],{"class":1667}," |",[1020,4326,4327],{"class":1688}," 'all'",[1020,4329,4330],{"class":1681}," } ",[1020,4332,4333],{"class":1667},"=",[1020,4335,4336],{"class":1681}," {})",[1020,4338,503],{"class":1667},[1020,4340,4341],{"class":1748}," MiddlewareHandler",[1020,4343,4344],{"class":1681},"\u003C",[1020,4346,4347],{"class":1748},"AuthEnv",[1020,4349,4350],{"class":1681},"> {\n",[1020,4352,4353,4356,4359,4361,4364,4366],{"class":1022,"line":1739},[1020,4354,4355],{"class":1667},"  const",[1020,4357,4358],{"class":1671}," mode",[1020,4360,1675],{"class":1667},[1020,4362,4363],{"class":1681}," opts.method ",[1020,4365,1721],{"class":1667},[1020,4367,4368],{"class":1688}," 'mutate-only'\n",[1020,4370,4371,4374,4377,4379,4382,4384,4387,4389,4392],{"class":1022,"line":1764},[1020,4372,4373],{"class":1667},"  return",[1020,4375,4376],{"class":1667}," async",[1020,4378,1780],{"class":1681},[1020,4380,4381],{"class":1708},"c",[1020,4383,581],{"class":1681},[1020,4385,4386],{"class":1708},"next",[1020,4388,3376],{"class":1681},[1020,4390,4391],{"class":1667},"=>",[1020,4393,4394],{"class":1681}," {\n",[1020,4396,4397,4400,4403,4405,4407,4410,4413,4416,4418,4421,4424,4427,4430],{"class":1022,"line":1769},[1020,4398,4399],{"class":1667},"    if",[1020,4401,4402],{"class":1681}," (mode ",[1020,4404,1685],{"class":1667},[1020,4406,4321],{"class":1688},[1020,4408,4409],{"class":1667}," &&",[1020,4411,4412],{"class":1667}," !",[1020,4414,4415],{"class":1671},"MUTATE_METHODS",[1020,4417,806],{"class":1681},[1020,4419,4420],{"class":1748},"has",[1020,4422,4423],{"class":1681},"(c.req.method)) ",[1020,4425,4426],{"class":1667},"return",[1020,4428,4429],{"class":1748}," next",[1020,4431,4432],{"class":1681},"()\n",[1020,4434,4435,4438,4440,4442,4444,4446,4449],{"class":1022,"line":1775},[1020,4436,4437],{"class":1667},"    const",[1020,4439,1705],{"class":1681},[1020,4441,464],{"class":1671},[1020,4443,4330],{"class":1681},[1020,4445,4333],{"class":1667},[1020,4447,4448],{"class":1748}," authCtx",[1020,4450,4451],{"class":1681},"(c)\n",[1020,4453,4454,4456,4459,4461,4463,4466],{"class":1022,"line":1794},[1020,4455,4437],{"class":1667},[1020,4457,4458],{"class":1671}," db",[1020,4460,1675],{"class":1667},[1020,4462,3390],{"class":1667},[1020,4464,4465],{"class":1748}," getDb",[1020,4467,4468],{"class":1681},"(c.env, c.executionCtx)\n",[1020,4470,4471,4473,4476,4478,4480,4483,4486,4489,4491,4494,4497,4500,4503,4505,4508,4511,4514,4516,4518],{"class":1022,"line":1799},[1020,4472,4437],{"class":1667},[1020,4474,4475],{"class":1671}," rows",[1020,4477,1675],{"class":1667},[1020,4479,3390],{"class":1667},[1020,4481,4482],{"class":1681}," db.",[1020,4484,4485],{"class":1748},"select",[1020,4487,4488],{"class":1681},"({",[1020,4490,2881],{"class":1667},[1020,4492,4493],{"class":1681},"}).",[1020,4495,4496],{"class":1748},"from",[1020,4498,4499],{"class":1681},"(company).",[1020,4501,4502],{"class":1748},"where",[1020,4504,1752],{"class":1681},[1020,4506,4507],{"class":1748},"eq",[1020,4509,4510],{"class":1681},"(company.id, companyId)).",[1020,4512,4513],{"class":1748},"limit",[1020,4515,1752],{"class":1681},[1020,4517,696],{"class":1671},[1020,4519,4520],{"class":1681},")\n",[1020,4522,4523,4525,4528,4530,4533,4536],{"class":1022,"line":3068},[1020,4524,4437],{"class":1667},[1020,4526,4527],{"class":1671}," row",[1020,4529,1675],{"class":1667},[1020,4531,4532],{"class":1681}," rows[",[1020,4534,4535],{"class":1671},"0",[1020,4537,4538],{"class":1681},"]\n",[1020,4540,4541,4543,4545,4547,4550,4553,4556,4559,4561,4564],{"class":1022,"line":3083},[1020,4542,4399],{"class":1667},[1020,4544,1780],{"class":1681},[1020,4546,1783],{"class":1667},[1020,4548,4549],{"class":1681},"row) ",[1020,4551,4552],{"class":1667},"throw",[1020,4554,4555],{"class":1681}," errors.",[1020,4557,4558],{"class":1748},"notFound",[1020,4560,1752],{"class":1681},[1020,4562,4563],{"class":1688},"'company'",[1020,4565,4520],{"class":1681},[1020,4567,4569,4571,4574,4576,4579],{"class":1022,"line":4568},12,[1020,4570,4399],{"class":1667},[1020,4572,4573],{"class":1681}," (row.approvalState ",[1020,4575,3323],{"class":1667},[1020,4577,4578],{"class":1688}," 'approved'",[1020,4580,4581],{"class":1681},") {\n",[1020,4583,4585,4588,4590,4593,4595,4598],{"class":1022,"line":4584},13,[1020,4586,4587],{"class":1667},"      throw",[1020,4589,4555],{"class":1681},[1020,4591,4592],{"class":1748},"forbidden",[1020,4594,1752],{"class":1681},[1020,4596,4597],{"class":1735},"\u002F* pending \u002F rejected 별 메시지 *\u002F",[1020,4599,4520],{"class":1681},[1020,4601,4603],{"class":1022,"line":4602},14,[1020,4604,4605],{"class":1681},"    }\n",[1020,4607,4609,4612,4614],{"class":1022,"line":4608},15,[1020,4610,4611],{"class":1667},"    await",[1020,4613,4429],{"class":1748},[1020,4615,4432],{"class":1681},[1020,4617,4619],{"class":1022,"line":4618},16,[1020,4620,4621],{"class":1681},"  }\n",[1020,4623,4625],{"class":1022,"line":4624},17,[1020,4626,3017],{"class":1681},[238,4628,4629,4635,4641,4651],{},[241,4630,4631,4632,4634],{},"기본 ",[32,4633,4223],{}," — GET·HEAD·OPTIONS는 통과 (조회 허용)",[241,4636,4637,4640],{},[32,4638,4639],{},"'all'"," 옵션 — 조회까지 차단 (필요 시)",[241,4642,4643,4646,4647,4650],{},[32,4644,4645],{},"requireAuth()"," 다음 체인 — ",[32,4648,4649],{},"authCtx","로 companyId 획득",[241,4652,4653],{},"매 요청 1회 SELECT (Hyperdrive 캐시 효과 기대)",[14,4655,4657],{"id":4656},"_82-18-라우트-일괄-적용","8.2 18 라우트 일괄 적용",[19,4659,4660],{},"대상:",[143,4662,4663,4675],{},[146,4664,4665],{},[149,4666,4667,4669,4672],{},[152,4668,2262],{},[152,4670,4671],{},"변수명",[152,4673,4674],{},"비고",[165,4676,4677,4700,4714,4727],{},[149,4678,4679,4682,4697],{},[170,4680,4681],{},"send · contacts · contact-groups · optout-entries · sender-phones · rcs-brands · email-domains · push-certs · kakao-sender-profiles · kakao-profile-groups · optout-080-numbers · templates · template-categories · landing-pages · flow-definitions · export-jobs · payment-methods · company-settings",[170,4683,4684,4687,4688,54,4691,54,4694,1488],{},[32,4685,4686],{},"app"," 또는 도메인별(",[32,4689,4690],{},"contacts",[32,4692,4693],{},"groups",[32,4695,4696],{},"phones",[170,4698,4699],{},"18종",[149,4701,4702,4708,4711],{},[170,4703,4704,4707],{},[23,4705,4706],{},"예외",": inquiries",[170,4709,4710],{},"—",[170,4712,4713],{},"승인 관련 문의 가능해야 함",[149,4715,4716,4722,4724],{},[170,4717,4718,4721],{},[23,4719,4720],{},"이미 §7",": me",[170,4723,4710],{},[170,4725,4726],{},"인라인 차단",[149,4728,4729,4735,4737],{},[170,4730,4731,4734],{},[23,4732,4733],{},"읽기 전용",": dispatch-history · credit-ledger",[170,4736,4710],{},[170,4738,4739],{},"GET만 정의, 차단 무영향",[19,4741,4742],{},"자동화 — 변수명을 grep으로 식별 후 perl로 import + use 2 군데 일괄 갱신:",[1011,4744,4748],{"className":4745,"code":4746,"language":4747,"meta":1016,"style":1016},"language-bash shiki shiki-themes github-light github-dark","for f in \"${TARGETS[@]}\"; do\n  varname=$(grep -oE \"[a-zA-Z]+\\.use\\('\\\\*', requireAuth\\(\\)\\)\" \"src\u002Froutes\u002F$f.ts\" | head -1 | cut -d. -f1)\n  perl -i -pe \"...\"  # import 라인 + use 라인 갱신\ndone\n","bash",[32,4749,4750,4782,4837,4854],{"__ignoreMap":1016},[1020,4751,4752,4755,4758,4761,4764,4767,4770,4773,4776,4779],{"class":1022,"line":1023},[1020,4753,4754],{"class":1667},"for",[1020,4756,4757],{"class":1681}," f ",[1020,4759,4760],{"class":1667},"in",[1020,4762,4763],{"class":1688}," \"${",[1020,4765,4766],{"class":1681},"TARGETS",[1020,4768,4769],{"class":1688},"[",[1020,4771,4772],{"class":1667},"@",[1020,4774,4775],{"class":1688},"]}\"",[1020,4777,4778],{"class":1681},"; ",[1020,4780,4781],{"class":1667},"do\n",[1020,4783,4784,4787,4789,4792,4795,4798,4801,4804,4807,4810,4813,4816,4818,4821,4824,4826,4829,4832,4835],{"class":1022,"line":1029},[1020,4785,4786],{"class":1681},"  varname",[1020,4788,4333],{"class":1667},[1020,4790,4791],{"class":1681},"$(",[1020,4793,4794],{"class":1748},"grep",[1020,4796,4797],{"class":1671}," -oE",[1020,4799,4800],{"class":1688}," \"[a-zA-Z]+\\.use\\('",[1020,4802,4803],{"class":1671},"\\\\",[1020,4805,4806],{"class":1688},"*', requireAuth\\(\\)\\)\"",[1020,4808,4809],{"class":1688}," \"src\u002Froutes\u002F",[1020,4811,4812],{"class":1681},"$f",[1020,4814,4815],{"class":1688},".ts\"",[1020,4817,4324],{"class":1667},[1020,4819,4820],{"class":1748}," head",[1020,4822,4823],{"class":1671}," -1",[1020,4825,4324],{"class":1667},[1020,4827,4828],{"class":1748}," cut",[1020,4830,4831],{"class":1671}," -d.",[1020,4833,4834],{"class":1671}," -f1",[1020,4836,4520],{"class":1681},[1020,4838,4839,4842,4845,4848,4851],{"class":1022,"line":1035},[1020,4840,4841],{"class":1748},"  perl",[1020,4843,4844],{"class":1671}," -i",[1020,4846,4847],{"class":1671}," -pe",[1020,4849,4850],{"class":1688}," \"...\"",[1020,4852,4853],{"class":1735},"  # import 라인 + use 라인 갱신\n",[1020,4855,4856],{"class":1022,"line":1739},[1020,4857,4858],{"class":1667},"done\n",[19,4860,4861,4862,4865],{},"각 파일에 ",[32,4863,4864],{},"requireApproved"," 2번 등장(import + use) 확인 — 18 파일 × 2 = 36 매치.",[14,4867,4869],{"id":4868},"_83-라이브-e2e-production","8.3 라이브 e2e (Production)",[143,4871,4872,4882],{},[146,4873,4874],{},[149,4875,4876,4878,4880],{},[152,4877,683],{},[152,4879,686],{},[152,4881,689],{},[165,4883,4884,4897,4910,4922,4934,4946],{},[149,4885,4886,4888,4894],{},[170,4887,696],{},[170,4889,4890,4891],{},"미승인 사업자(corp) ",[23,4892,4893],{},"GET \u002Fcontacts",[170,4895,4896],{},"✅ 200 — 조회 허용",[149,4898,4899,4901,4907],{},[170,4900,707],{},[170,4902,4903,4904],{},"미승인 사업자 ",[23,4905,4906],{},"POST \u002Fcontacts",[170,4908,4909],{},"✅ 403 \"사업자등록증 심사 승인 후 이용할 수 있습니다.\"",[149,4911,4912,4914,4919],{},[170,4913,717],{},[170,4915,4903,4916],{},[23,4917,4918],{},"POST \u002Fsender-phones",[170,4920,4921],{},"✅ 403 동일 메시지",[149,4923,4924,4926,4931],{},[170,4925,729],{},[170,4927,4903,4928],{},[23,4929,4930],{},"POST \u002Fsend\u002Fsms",[170,4932,4933],{},"✅ 403 — 발송 차단",[149,4935,4936,4938,4943],{},[170,4937,739],{},[170,4939,4940,4941],{},"개인(approved) ",[23,4942,4906],{},[170,4944,4945],{},"✅ 201 정상 생성",[149,4947,4948,4950,4955],{},[170,4949,752],{},[170,4951,4903,4952],{},[23,4953,4954],{},"POST \u002Finquiries",[170,4956,4957],{},"✅ 차단 안 됨(400은 body validation) — 예외 정상",[19,4959,4960],{},"검증 데이터 cleanup 완료.",[14,4962,4964],{"id":4963},"_84-산출물","8.4 산출물",[238,4966,4967,4975],{},[241,4968,4969,4972,4973],{},[32,4970,4971],{},"malgn-noti-api: ?"," — 19 파일 변경(1 신규 + 18 라우트). Workers 배포 #16 Version ",[32,4974,4231],{},[241,4976,4977],{},"WBS 5-3C-17은 이미 ✅, 추가 갱신은 없음(같은 정책의 확장 적용)",[14,4979,4981],{"id":4980},"_85-알려진-한계-다음-작업","8.5 알려진 한계 \u002F 다음 작업",[238,4983,4984,4989,4999,5008],{},[241,4985,4986,4988],{},[23,4987,4156],{}," (3번) — 발송·이력·주소록 등 페이지에 접근 시 안내 배너 또는 기능 락 UI. 아직 페이지가 모두 목업 데이터 기반이라 백엔드 차단이 화면에 즉시 반영 안 됨 — 추후 페이지별 백엔드 연동 시 일관 처리 또는 별도 글로벌 안내 띠.",[241,4990,4991,4994,4995,4998],{},[23,4992,4993],{},"GNB 글로벌 안내 띠"," (4번) — ",[32,4996,4997],{},"auth.tenant.approvalState","를 GNB·셸 컴포넌트에서 구독해 모든 페이지 상단에 노출.",[241,5000,5001,5004,5005,5007],{},[23,5002,5003],{},"요청당 DB SELECT 1회"," — Hyperdrive 캐시로 빠르지만, 트래픽 증가 시 JWT claim에 ",[32,5006,3792],{},"를 넣어 단축 가능. 단 승인 후 사용자가 재로그인하기 전엔 갱신 안 됨 → 단기적으로 미적용 권장.",[241,5009,5010,5015],{},[23,5011,5012,5014],{},[32,5013,4227],{}," 외 예외"," — 추후 운영자단에서 첨부 파일 업로드(R2)·결제(PG 콜백) 등 필요 시 케이스별 검토.",[94,5017],{},[10,5019,5021,5022,5025],{"id":5020},"_9-사용자단-승인-게이트-ui-일관화-글로벌-띠-라우트-가드-home-안내-배포-65","§9. 사용자단 승인 게이트 UI 일관화 — 글로벌 띠 + 라우트 가드 + ",[32,5023,5024],{},"\u002Fhome"," 안내 (배포 #65)",[14,5027,103],{"id":5028},"한-줄-8",[19,5030,5031,5032,30,5035,5038,5039,30,5042,5045,5046,5048,5049,5054,5055,5058,5059,130],{},"§7·§8에서 백엔드(DB + 18 라우트 차단)로 정책 인프라화 완료. 사용자단도 일관 — ",[23,5033,5034],{},"글로벌 띠",[32,5036,5037],{},"AppApprovalBanner","(layout 최상단·GNB 위)로 모든 페이지에 승인 상태 알림 + ",[23,5040,5041],{},"글로벌 라우트 가드",[32,5043,5044],{},"middleware\u002Fapproval.global.ts","로 차단 페이지 접근 시 ",[32,5047,4174],{},"로 자동 리다이렉트 + ",[23,5050,5051,5053],{},[32,5052,5024],{}," 페이지","는 미승인 시 KPI\u002F채널\u002F통계 카드 대신 전체 화면 큰 안내(",[32,5056,5057],{},"approval-hero",")로 교체. Pages 배포 #65 (alias ",[32,5060,5061],{},"2eec9e0b.malgn-noti.pages.dev",[14,5063,5065,5066],{"id":5064},"_91-글로벌-띠-appapprovalbannervue","9.1 글로벌 띠 — ",[32,5067,5068],{},"AppApprovalBanner.vue",[19,5070,5071,4245],{},[255,5072,5074],{"href":5073},"..\u002F..\u002Fapp\u002Fcomponents\u002FAppApprovalBanner.vue","app\u002Fcomponents\u002FAppApprovalBanner.vue",[238,5076,5077,5087,5094,5101,5107],{},[241,5078,5079,5081,5082,2071,5084,5086],{},[32,5080,4997],{},"를 구독 → ",[32,5083,2288],{},[32,5085,3707],{},"일 때만 노출",[241,5088,5089,5090,5093],{},"pending: 노란색 띠(",[32,5091,5092],{},"#fff8e6"," + warning border) + 시계 아이콘",[241,5095,5096,5097,5100],{},"rejected: 빨간색 띠(",[32,5098,5099],{},"#fef2f2"," + danger border) + X 아이콘 + 반려 사유 인용",[241,5102,5103,5104,5106],{},"우측 버튼 — pending이면 \"회원 정보\", rejected면 \"다시 제출하기\" → ",[32,5105,4174],{},"로 이동",[241,5108,5109],{},"반응형(720px 미만 wrap)",[19,5111,5112,5116,5117,5120],{},[255,5113,5115],{"href":5114},"..\u002F..\u002Fapp\u002Flayouts\u002Fdefault.vue","app\u002Flayouts\u002Fdefault.vue","에 마운트 — ",[32,5118,5119],{},"AppGnb"," 위, layout 최상단:",[1011,5122,5126],{"className":5123,"code":5124,"language":5125,"meta":1016,"style":1016},"language-vue shiki shiki-themes github-light github-dark","\u003CAppApprovalBanner \u002F>\n\u003CAppGnb \u002F>\n\u003Cmain>\u003Cslot \u002F>\u003C\u002Fmain>\n\u003CAppFooter \u002F>\n","vue",[32,5127,5128,5138,5146,5161],{"__ignoreMap":1016},[1020,5129,5130,5132,5135],{"class":1022,"line":1023},[1020,5131,4344],{"class":1681},[1020,5133,5037],{"class":5134},"s9eBZ",[1020,5136,5137],{"class":1681}," \u002F>\n",[1020,5139,5140,5142,5144],{"class":1022,"line":1029},[1020,5141,4344],{"class":1681},[1020,5143,5119],{"class":5134},[1020,5145,5137],{"class":1681},[1020,5147,5148,5150,5153,5156,5158],{"class":1022,"line":1035},[1020,5149,4344],{"class":1681},[1020,5151,5152],{"class":5134},"main",[1020,5154,5155],{"class":1681},">\u003Cslot \u002F>\u003C\u002F",[1020,5157,5152],{"class":5134},[1020,5159,5160],{"class":1681},">\n",[1020,5162,5163,5165,5168],{"class":1022,"line":1739},[1020,5164,4344],{"class":1681},[1020,5166,5167],{"class":5134},"AppFooter",[1020,5169,5137],{"class":1681},[19,5171,5172],{},"→ 모든 인증 페이지에서 자동 노출.",[14,5174,5176,5177],{"id":5175},"_92-글로벌-라우트-가드-middlewareapprovalglobalts","9.2 글로벌 라우트 가드 — ",[32,5178,5044],{},[19,5180,5181,4245],{},[255,5182,5184],{"href":5183},"..\u002F..\u002Fapp\u002Fmiddleware\u002Fapproval.global.ts","app\u002Fmiddleware\u002Fapproval.global.ts",[1011,5186,5188],{"className":1658,"code":5187,"language":1660,"meta":1016,"style":1016},"const ALLOWED_PREFIXES = ['\u002Faccount', '\u002Fhome', '\u002Fhelp', '\u002Fguide', '\u002Fwbs', '\u002Finquiry']\n\nexport default defineNuxtRouteMiddleware((to) => {\n  if (to.meta.auth === false) return\n  const state = useAuthStore().tenant?.approvalState\n  if (!state || state === 'approved') return  \u002F\u002F 미hydrate면 통과\n  if (ALLOWED_PREFIXES.some(p => to.path === p || to.path.startsWith(`${p}\u002F`))) return\n  return navigateTo('\u002Faccount\u002Fsettings')\n})\n",[32,5189,5190,5232,5236,5258,5275,5290,5318,5370,5384],{"__ignoreMap":1016},[1020,5191,5192,5194,5197,5199,5202,5205,5207,5210,5212,5215,5217,5220,5222,5225,5227,5230],{"class":1022,"line":1023},[1020,5193,1668],{"class":1667},[1020,5195,5196],{"class":1671}," ALLOWED_PREFIXES",[1020,5198,1675],{"class":1667},[1020,5200,5201],{"class":1681}," [",[1020,5203,5204],{"class":1688},"'\u002Faccount'",[1020,5206,581],{"class":1681},[1020,5208,5209],{"class":1688},"'\u002Fhome'",[1020,5211,581],{"class":1681},[1020,5213,5214],{"class":1688},"'\u002Fhelp'",[1020,5216,581],{"class":1681},[1020,5218,5219],{"class":1688},"'\u002Fguide'",[1020,5221,581],{"class":1681},[1020,5223,5224],{"class":1688},"'\u002Fwbs'",[1020,5226,581],{"class":1681},[1020,5228,5229],{"class":1688},"'\u002Finquiry'",[1020,5231,4538],{"class":1681},[1020,5233,5234],{"class":1022,"line":1029},[1020,5235,1730],{"emptyLinePlaceholder":1729},[1020,5237,5238,5240,5243,5246,5249,5252,5254,5256],{"class":1022,"line":1035},[1020,5239,4298],{"class":1667},[1020,5241,5242],{"class":1667}," default",[1020,5244,5245],{"class":1748}," defineNuxtRouteMiddleware",[1020,5247,5248],{"class":1681},"((",[1020,5250,5251],{"class":1708},"to",[1020,5253,3376],{"class":1681},[1020,5255,4391],{"class":1667},[1020,5257,4394],{"class":1681},[1020,5259,5260,5263,5266,5268,5271,5273],{"class":1022,"line":1739},[1020,5261,5262],{"class":1667},"  if",[1020,5264,5265],{"class":1681}," (to.meta.auth ",[1020,5267,1685],{"class":1667},[1020,5269,5270],{"class":1671}," false",[1020,5272,3376],{"class":1681},[1020,5274,1761],{"class":1667},[1020,5276,5277,5279,5282,5284,5287],{"class":1022,"line":1764},[1020,5278,4355],{"class":1667},[1020,5280,5281],{"class":1671}," state",[1020,5283,1675],{"class":1667},[1020,5285,5286],{"class":1748}," useAuthStore",[1020,5288,5289],{"class":1681},"().tenant?.approvalState\n",[1020,5291,5292,5294,5296,5298,5301,5304,5307,5309,5311,5313,5315],{"class":1022,"line":1769},[1020,5293,5262],{"class":1667},[1020,5295,1780],{"class":1681},[1020,5297,1783],{"class":1667},[1020,5299,5300],{"class":1681},"state ",[1020,5302,5303],{"class":1667},"||",[1020,5305,5306],{"class":1681}," state ",[1020,5308,1685],{"class":1667},[1020,5310,4578],{"class":1688},[1020,5312,3376],{"class":1681},[1020,5314,4426],{"class":1667},[1020,5316,5317],{"class":1735},"  \u002F\u002F 미hydrate면 통과\n",[1020,5319,5320,5322,5324,5327,5329,5332,5334,5336,5339,5342,5344,5347,5349,5352,5355,5357,5360,5362,5365,5368],{"class":1022,"line":1775},[1020,5321,5262],{"class":1667},[1020,5323,1780],{"class":1681},[1020,5325,5326],{"class":1671},"ALLOWED_PREFIXES",[1020,5328,806],{"class":1681},[1020,5330,5331],{"class":1748},"some",[1020,5333,1752],{"class":1681},[1020,5335,19],{"class":1708},[1020,5337,5338],{"class":1667}," =>",[1020,5340,5341],{"class":1681}," to.path ",[1020,5343,1685],{"class":1667},[1020,5345,5346],{"class":1681}," p ",[1020,5348,5303],{"class":1667},[1020,5350,5351],{"class":1681}," to.path.",[1020,5353,5354],{"class":1748},"startsWith",[1020,5356,1752],{"class":1681},[1020,5358,5359],{"class":1688},"`${",[1020,5361,19],{"class":1681},[1020,5363,5364],{"class":1688},"}\u002F`",[1020,5366,5367],{"class":1681},"))) ",[1020,5369,1761],{"class":1667},[1020,5371,5372,5374,5377,5379,5382],{"class":1022,"line":1794},[1020,5373,4373],{"class":1667},[1020,5375,5376],{"class":1748}," navigateTo",[1020,5378,1752],{"class":1681},[1020,5380,5381],{"class":1688},"'\u002Faccount\u002Fsettings'",[1020,5383,4520],{"class":1681},[1020,5385,5386],{"class":1022,"line":1799},[1020,5387,5388],{"class":1681},"})\n",[19,5390,5391],{},"허용 경로:",[238,5393,5394,5400,5405,5417],{},[241,5395,5396,5399],{},[32,5397,5398],{},"\u002Faccount\u002F*"," — 회원 정보·승인 안내·재제출",[241,5401,5402,5404],{},[32,5403,5024],{}," — 큰 안내 카드(다음 절)",[241,5406,5407,54,5410,54,5413,5416],{},[32,5408,5409],{},"\u002Fhelp",[32,5411,5412],{},"\u002Fguide",[32,5414,5415],{},"\u002Fwbs"," — 정적 문서",[241,5418,5419,54,5422,5425],{},[32,5420,5421],{},"\u002Finquiry",[32,5423,5424],{},"\u002Faccount\u002Finquiry"," — 1:1 문의 (백엔드도 §8에서 예외)",[19,5427,5428,5429,5431],{},"차단 경로(자동 리다이렉트 → ",[32,5430,4174],{},"):",[238,5433,5434],{},[241,5435,5436,2105,5438,2105,5441,2105,5444,2105,5447,2105,5450,2105,5453],{},[32,5437,4165],{},[32,5439,5440],{},"\u002Fhistory\u002F*",[32,5442,5443],{},"\u002Fcontacts\u002F*",[32,5445,5446],{},"\u002Fsender\u002F*",[32,5448,5449],{},"\u002Fmanage\u002F*",[32,5451,5452],{},"\u002Fcampaign*",[32,5454,5455],{},"\u002Fcharge*",[19,5457,5458,5459,5462,5463,5466],{},"SSR 안전: store 미hydrate(",[32,5460,5461],{},"state === undefined",")면 통과. 클라이언트 부트스트랩이 ",[32,5464,5465],{},"fetchMe()","로 hydrate한 다음 재진입 시 작동.",[14,5468,5470,5471,5473],{"id":5469},"_93-home-페이지-미승인-분기","9.3 ",[32,5472,5024],{}," 페이지 미승인 분기",[19,5475,5476,512,5480,5483],{},[255,5477,5479],{"href":5478},"..\u002F..\u002Fapp\u002Fpages\u002Fhome.vue","app\u002Fpages\u002Fhome.vue",[32,5481,5482],{},"v-if=\"isLocked\"","로 두 화면 분기:",[19,5485,5486,5487,5431],{},"미승인 화면(",[32,5488,5057],{},[238,5490,5491,5494,5497,5500,5503],{},[241,5492,5493],{},"중앙 정렬 큰 카드 (max-width 720px)",[241,5495,5496],{},"72px 시계\u002FX 아이콘 + 24px 제목",[241,5498,5499],{},"본문(pending: \"심사 중, 보통 영업일 1~2일\" \u002F rejected: 사유 인용)",[241,5501,5502],{},"CTA 2개: \"회원 정보 확인하기\u002F다시 제출하기\" + \"1:1 문의 작성\"",[241,5504,5505],{},"하단 현재 상태 메타",[19,5507,5508],{},"승인 상태(기본):",[238,5510,5511],{},[241,5512,5513],{},"기존 KPI·채널·통계 카드 그대로 (변경 없음)",[14,5515,5517],{"id":5516},"_94-흐름-정리-가입-후-미승인-사용자가-경험하는-ux","9.4 흐름 정리 — 가입 후 미승인 사용자가 경험하는 UX",[408,5519,5520,5529,5536,5542,5554,5561],{},[241,5521,5522,5525,5526,5528],{},[23,5523,5524],{},"회원가입 완료"," → 자동 로그인 → ",[32,5527,5024],{},"으로 이동",[241,5530,5531,5535],{},[23,5532,5533],{},[32,5534,5024],{},": 큰 안내 카드 \"사업자등록증 심사 중입니다 … 보통 영업일 1~2일\" + CTA \"회원 정보 확인하기\"",[241,5537,5538,5541],{},[23,5539,5540],{},"상단 글로벌 띠",": 모든 페이지에 항상 노출 (회원 정보·문의 등 허용 페이지에서도)",[241,5543,5544,560,5547,5550,5551,5553],{},[23,5545,5546],{},"차단 페이지 시도",[32,5548,5549],{},"\u002Fsend\u002Fsms"," 등 클릭 시 미들웨어가 즉시 ",[32,5552,4174],{},"로 리다이렉트",[241,5555,5556,5560],{},[23,5557,5558],{},[32,5559,4174],{},": 가입 정보 상단 배너(§7) + 모든 입력 disabled",[241,5562,5563,5566],{},[23,5564,5565],{},"승인 완료 후",": store 갱신 시점부터 띠 사라짐 + 모든 페이지 정상 접근 가능",[14,5568,5570],{"id":5569},"_95-산출물","9.5 산출물",[238,5572,5573,5586],{},[241,5574,3520,5575,5577,5578,5580,5581,5577,5583,5585],{},[32,5576,5074],{}," 신규 + ",[32,5579,5115],{}," 마운트 + ",[32,5582,5184],{},[32,5584,5479],{}," 미승인 분기",[241,5587,5588,5589],{},"Pages 배포 #65 alias ",[32,5590,5061],{},[14,5592,5594],{"id":5593},"_96-알려진-한계-다음-작업","9.6 알려진 한계 \u002F 다음 작업",[238,5596,5597,5607,5613,5626,5632],{},[241,5598,5599,5602,5603,5606],{},[23,5600,5601],{},"GNB 메뉴 항목 disabled"," — 현재 미들웨어가 리다이렉트로 처리하지만, 시각적으로 GNB 메뉴는 그대로 활성 표시. 메뉴 항목별 ",[32,5604,5605],{},"disabled"," 클래스 + 호버 시 사유 툴팁이 더 친절. 후속.",[241,5608,5609,5612],{},[23,5610,5611],{},"승인 완료 후 자동 새로고침"," — 현재는 사용자가 직접 새로고침해야 새 상태 반영. WebSocket 또는 폴링으로 자동 갱신 검토.",[241,5614,5615,5620,5621,54,5623,5625],{},[23,5616,5617,5619],{},[32,5618,5424],{},"도 허용"," — 1:1 문의 작성은 미승인 시에도 가능해야 함. 현재 ",[32,5622,5421],{},[32,5624,5424],{}," 두 경로 모두 허용 처리 — 라우트 구조 확정 후 정리.",[241,5627,5628,5631],{},[23,5629,5630],{},"모바일 띠 줄바꿈"," — 720px 미만에서 텍스트 wrap. 실제 모바일에서 가독성 점검 필요.",[241,5633,5634,5640],{},[23,5635,5636,5639],{},[32,5637,5638],{},"\u002Faccount\u002Fcontract"," 분기"," — pending 상태에서 계약서 화면이 어떻게 보일지 정책 결정 필요.",[94,5642],{},[10,5644,5646,5647,5649],{"id":5645},"_10-미승인-accountcontract-리다이렉트-정책-배포-66","§10. 미승인 → ",[32,5648,5638],{}," 리다이렉트 정책 (배포 #66)",[14,5651,103],{"id":5652},"한-줄-9",[19,5654,5655,5656,5658,5659,5665,5666,5668,5669,1516,5671,5673,5674,5677,5678,130],{},"§9의 \"미승인 사용자 진입점은 ",[32,5657,5024],{},"\"을 변경 — ",[23,5660,5661,5662,5664],{},"사용자 정책 결정으로 미승인 사용자는 가입 직후·로그인 시·차단 페이지 접근 시 모두 ",[32,5663,5638],{}," (계약 관리 — 사업자등록증 제출\u002F재제출 화면)로 이동",". ",[32,5667,5024],{},"도 차단 페이지에 포함, 미들웨어 리다이렉트 대상도 ",[32,5670,4174],{},[32,5672,5638],{}," 변경. 가입 마법사 Step 5 버튼은 유형 분기(\"계약 관리로 이동\" \u002F \"대시보드로 이동\"). AppApprovalBanner의 CTA도 \"회원 정보\" → \"계약 관리\". ",[32,5675,5676],{},"\u002Fhome.vue","의 §9에서 추가했던 미승인 분기 코드는 진입 자체가 불가능해진 결과 불필요 → 제거. Pages 배포 #66 (alias ",[32,5679,5680],{},"5256d66d.malgn-noti.pages.dev",[14,5682,5684],{"id":5683},"_101-변경-사항","10.1 변경 사항",[143,5686,5687,5695],{},[146,5688,5689],{},[149,5690,5691,5693],{},[152,5692,1112],{},[152,5694,1115],{},[165,5696,5697,5714,5730,5753],{},[149,5698,5699,5703],{},[170,5700,5701],{},[255,5702,5044],{"href":5183},[170,5704,5705,476,5707,5709,5710,1516,5712],{},[32,5706,5326],{},[32,5708,5024],{}," 제거. 리다이렉트 대상 ",[32,5711,4174],{},[32,5713,5638],{},[149,5715,5716,5721],{},[170,5717,5718],{},[255,5719,5720],{"href":5073},"components\u002FAppApprovalBanner.vue",[170,5722,5723,1516,5726,5729],{},[32,5724,5725],{},"goToSettings()",[32,5727,5728],{},"goToContract()",". CTA 텍스트 pending \"회원 정보\" → \"계약 관리\" \u002F rejected \"다시 제출하기\"(유지)",[149,5731,5732,5738],{},[170,5733,5734],{},[255,5735,5737],{"href":5736},"..\u002F..\u002Fapp\u002Fpages\u002Fsignup.vue","pages\u002Fsignup.vue",[170,5739,5740,512,5743,5746,5747,5749,5750,5752],{},[32,5741,5742],{},"finish()",[32,5744,5745],{},"isBusiness","이면 ",[32,5748,5638],{},", 개인이면 ",[32,5751,5024],{},". 버튼 라벨도 분기 (\"계약 관리로 이동\" \u002F \"대시보드로 이동\")",[149,5754,5755,5760],{},[170,5756,5757],{},[255,5758,5759],{"href":5478},"pages\u002Fhome.vue",[170,5761,5762,5763,122,5765,5767],{},"§9에서 추가한 미승인 분기(",[32,5764,5482],{},[32,5766,5057],{}," 카드 + 스타일) 모두 제거 — 미승인 사용자는 미들웨어가 차단해 진입 자체가 안 됨. 코드 단순화",[14,5769,5771],{"id":5770},"_102-새-흐름-미승인-사용자가-경험하는-ux-정리","10.2 새 흐름 — 미승인 사용자가 경험하는 UX (정리)",[408,5773,5774,5785,5793,5800,5814,5824],{},[241,5775,5776,5778,5779,5781,5782,5784],{},[23,5777,5524],{}," → Step 5 → 클릭 시 자동으로 ",[32,5780,5638],{}," (사업자) \u002F ",[32,5783,5024],{}," (개인)",[241,5786,5787,5790,5791,5553],{},[23,5788,5789],{},"로그인"," → fetchMe → store에 approvalState='pending' → \u002Fhome 진입 시도 → 미들웨어가 ",[32,5792,5638],{},[241,5794,5795,5797,5798],{},[23,5796,5540],{},": 모든 페이지에 항상 노출 (\"사업자등록증 심사 중 …\") + CTA \"계약 관리\" → ",[32,5799,5638],{},[241,5801,5802,1780,5805,581,5807,5810,5811,5813],{},[23,5803,5804],{},"다른 페이지 시도",[32,5806,5549],{},[32,5808,5809],{},"\u002Fcontacts\u002Flist"," 등): 미들웨어가 즉시 ",[32,5812,5638],{},"로 자동 리다이렉트",[241,5815,5816,5820,5821,1488],{},[23,5817,5818],{},[32,5819,5638],{},": 사용자가 사업자등록증 제출\u002F재제출 가능 (",[32,5822,5823],{},"AppContractPanel",[241,5825,5826,5828,5829,5831],{},[23,5827,5565],{},": store 갱신 시점부터 모든 페이지 정상 접근 + ",[32,5830,5024],{},"도 진입 가능",[14,5833,5835],{"id":5834},"_103-허용-페이지-변경-후","10.3 허용 페이지 (변경 후)",[143,5837,5838,5847],{},[146,5839,5840],{},[149,5841,5842,5845],{},[152,5843,5844],{},"경로",[152,5846,160],{},[165,5848,5849,5862,5875,5886],{},[149,5850,5851,5855],{},[170,5852,5853],{},[32,5854,5398],{},[170,5856,5857,5858,5861],{},"회원 정보·",[23,5859,5860],{},"계약 관리(메인 진입점)","·문의 등",[149,5863,5864,5872],{},[170,5865,5866,54,5868,54,5870],{},[32,5867,5409],{},[32,5869,5412],{},[32,5871,5415],{},[170,5873,5874],{},"정적 문서",[149,5876,5877,5883],{},[170,5878,5879,54,5881],{},[32,5880,5421],{},[32,5882,5424],{},[170,5884,5885],{},"1:1 문의",[149,5887,5888,5893],{},[170,5889,5890],{},[32,5891,5892],{},"meta.auth === false",[170,5894,5895],{},"로그인·가입·재설정 등",[19,5897,5898,5899,5431],{},"차단 (자동 리다이렉트 → ",[32,5900,5638],{},[238,5902,5903,5908],{},[241,5904,5905,5907],{},[32,5906,5024],{}," (NEW — §9에서는 허용이었음)",[241,5909,5910,54,5912,54,5914,54,5916,54,5918,54,5920,54,5922,5924],{},[32,5911,4165],{},[32,5913,5440],{},[32,5915,5443],{},[32,5917,5446],{},[32,5919,5449],{},[32,5921,5452],{},[32,5923,5455],{}," 등 모든 변경 페이지",[14,5926,5928],{"id":5927},"_104-산출물","10.4 산출물",[238,5930,5931,5934],{},[241,5932,5933],{},"사용자단: 4 파일 수정 — middleware\u002Fapproval.global.ts · components\u002FAppApprovalBanner.vue · pages\u002Fsignup.vue · pages\u002Fhome.vue(미승인 분기 + 스타일 ~80줄 제거)",[241,5935,5936,5937],{},"Pages 배포 #66 alias ",[32,5938,5680],{},[14,5940,5942],{"id":5941},"_105-알려진-한계-다음-작업","10.5 알려진 한계 \u002F 다음 작업",[238,5944,5945,5953,5965,5971],{},[241,5946,5947,5952],{},[23,5948,5949,5951],{},[32,5950,5823],{},"의 미승인 UX 최적화"," — 현재 컴포넌트는 가입 후 일반 흐름(승인된 사용자의 계약 갱신 등) 기준으로 디자인됨. 미승인(처음 제출) \u002F 반려(재제출) 상태에 따라 화면 헤더·CTA를 더 명확히 분기할 여지. 후속.",[241,5954,5955,512,5958,5960,5961,5964],{},[23,5956,5957],{},"사업자등록증 업로드 실 API",[32,5959,5823],{},"의 업로드 모달은 현재 UI만. R2 업로드 + ",[32,5962,5963],{},"TB_CONTRACT_FILE"," 적재 라우트 필요.",[241,5966,5967,5970],{},[23,5968,5969],{},"계약 재제출 후 상태 전이"," — 운영자가 다시 'pending'으로 돌리는 흐름 + 사용자에게 알림 필요.",[241,5972,5973,5979],{},[23,5974,5975,5976,5978],{},"개인 가입자의 ",[32,5977,5638],{}," 비노출"," — 메뉴에서 숨김 처리 검토(현재 모두 노출).",[94,5981],{},[10,5983,5985,5986,5988],{"id":5984},"_11-accountcontract-실-api-r2-첨부-인프라-배포-17-69","§11. ",[32,5987,5638],{}," 실 API + R2 첨부 인프라 (배포 #17 \u002F #69)",[14,5990,103],{"id":5991},"한-줄-10",[19,5993,5994,5995,5997,5998,6001,6002,6005,6006,30,6009,6012,6013,1480,6015,54,6018,6020,6021,1480,6024,6027,6028,30,6031,6034,6035,6037,6038,581,6041,6044,6045,30,6048,6051,6052,54,6055,54,6058,6061,6062,6064,6065,6068,6069,6072,6073,6076,6077,6080,6081,6084,6085,6088],{},"§10에서 정책 정합화한 ",[32,5996,5638],{},"의 실 백엔드 연동. ",[23,5999,6000],{},"(a)"," R2 bucket ",[32,6003,6004],{},"malgn-noti-files"," 생성 + ",[32,6007,6008],{},"wrangler.toml",[32,6010,6011],{},"FILES"," 바인딩 + ",[32,6014,2663],{},[32,6016,6017],{},"TB_CONTRACT",[32,6019,5963],{}," 정의(라이브 DDL과 일치) + Hono ",[32,6022,6023],{},"AuthBindings",[32,6025,6026],{},"FILES?: R2Bucket"," 옵셔널 추가. ",[23,6029,6030],{},"(b)",[32,6032,6033],{},"POST \u002Fauth\u002Fsignup"," 확장 — 사업자(corp\u002Fsole) 가입 시 NICE 소비 직후 ",[32,6036,6017],{}," 'initial' 자동 1건 생성(",[32,6039,6040],{},"title='최초 이용계약 온라인체결'",[32,6042,6043],{},"version='신규'","). ",[23,6046,6047],{},"(c)",[32,6049,6050],{},"\u002Fcontracts"," 라우트 신설 5 엔드포인트 — list \u002F sign \u002F files list \u002F files upload(multipart) \u002F files download(stream) \u002F files delete. PDF·10MB 제한, name 접두사(",[32,6053,6054],{},"사업자등록증_…",[32,6056,6057],{},"대부업등록증_…",[32,6059,6060],{},"지급이행보증보험증권_…",")로 종류 구분(",[32,6063,5963],{},"에 kind 컬럼 없음). 회사 단위 권한 — companyId 매칭 안 되면 404. renew 체결 시 같은 회사의 다른 done 계약은 자동 expired. ",[23,6066,6067],{},"(d)"," OpenAPI 5 paths + 2 schemas 추가. ",[23,6070,6071],{},"(e)"," 프런트 ",[32,6074,6075],{},"AppContractPanel.vue"," 전면 교체 — 목업 contracts\u002FbizFiles\u002FloanFiles\u002FinsuranceFiles 제거, ",[32,6078,6079],{},"await Promise.all([loadContracts(), loadFiles()])"," SSR, FormData multipart POST, 미리보기는 인증 fetch → blob → object URL(iframe은 Authorization 헤더 못 실음). Workers 배포 #17(Version ",[32,6082,6083],{},"7213946f-42c4-4772-bfbe-e8d271167a01","), Pages 배포 alias ",[32,6086,6087],{},"9808fe42.malgn-noti.pages.dev",". 라이브 e2e 4 시나리오 통과(corp signup auto-contract \u002F 파일 업로드 R2 \u002F 파일 목록 \u002F 체결 → done+expires).",[14,6090,6092,6093,6096,6097,6099],{"id":6091},"_111-결정-tb_contract_filekind-컬럼-없음-name-접두사-사용","11.1 결정 — ",[32,6094,6095],{},"TB_CONTRACT_FILE.kind"," 컬럼 없음 → ",[32,6098,1992],{}," 접두사 사용",[19,6101,6102,6103,6105,6106,6109,6110,6116,6117,6120],{},"라이브 ",[32,6104,5963],{}," 스키마는 ",[32,6107,6108],{},"id \u002F contract_id \u002F name \u002F size_bytes \u002F r2_key \u002F uploaded_at"," 6 컬럼. 파일 종류(사업자등록증·대부업등록증·보험증권)를 구분할 컬럼이 없음. 새 DDL을 발행하기보다는 ",[23,6111,6112,6113,6115],{},"업로드 시 ",[32,6114,1992],{},"에 한국어 라벨 접두사를 붙여 저장",", 프런트에서 ",[32,6118,6119],{},"startsWith('사업자등록증_')"," 등으로 분류. 라이브 DDL 추가 없이 정책 흡수.",[143,6122,6123,6133],{},[146,6124,6125],{},[149,6126,6127,6130],{},[152,6128,6129],{},"kind 폼 필드",[152,6131,6132],{},"name 접두사",[165,6134,6135,6147,6159],{},[149,6136,6137,6142],{},[170,6138,6139],{},[32,6140,6141],{},"biz",[170,6143,6144],{},[32,6145,6146],{},"사업자등록증_\u003C원본파일명>",[149,6148,6149,6154],{},[170,6150,6151],{},[32,6152,6153],{},"loan",[170,6155,6156],{},[32,6157,6158],{},"대부업등록증_\u003C원본파일명>",[149,6160,6161,6166],{},[170,6162,6163],{},[32,6164,6165],{},"insurance",[170,6167,6168],{},[32,6169,6170],{},"지급이행보증보험증권_\u003C원본파일명>",[19,6172,6173,6174,54,6177,54,6179,6182],{},"R2 customMetadata에는 ",[32,6175,6176],{},"kind",[32,6178,464],{},[32,6180,6181],{},"contractId"," 모두 기록 — 라이브 운영 중 별도 보고서\u002F감사에 활용 가능.",[14,6184,6186],{"id":6185},"_112-r2-bucket-바인딩","11.2 R2 bucket + 바인딩",[238,6188,6189,6195,6202,6216],{},[241,6190,6191,6194],{},[32,6192,6193],{},"npx wrangler@4 r2 bucket create malgn-noti-files --remote"," → 신규 버킷",[241,6196,6197,1480,6199,3773],{},[32,6198,6008],{},[32,6200,6201],{},"[[r2_buckets]] binding = \"FILES\" \u002F bucket_name = \"malgn-noti-files\"",[241,6203,6204,1829,6207,1480,6209,6211,6212,6215],{},[32,6205,6206],{},"src\u002Fmiddleware\u002Fauth.ts",[32,6208,6023],{},[32,6210,6026],{}," 옵셔널 추가 — 기존 라우트가 모두 ",[32,6213,6214],{},"Hono\u003CAuthEnv>()"," 패턴이라 한 곳에서 형 변경하면 자동 전파",[241,6217,6218,6219,6222],{},"사용처에서 ",[32,6220,6221],{},"if (!c.env.FILES) throw errors.internal(...)"," 가드 — 로컬 dev에서 바인딩 누락 시 503 응답으로 빠르게 실패",[14,6224,6226],{"id":6225},"_113-schemats-라이브-ddl과-정합","11.3 schema.ts — 라이브 DDL과 정합",[19,6228,6229,6231,6232,54,6234,6236],{},[255,6230,783],{"href":1124}," — 라이브에 이미 존재하던 ",[32,6233,6017],{},[32,6235,5963],{}," 정의 추가:",[238,6238,6239,6285],{},[241,6240,6241,512,6244,6246,6247,6250,6251,1225,6253,1225,6256,1225,6259,6262,6263,1225,6266,6250,6269,1225,6271,1225,6274,1225,6276,1225,6278,6281,6282],{},[32,6242,6243],{},"contract",[32,6245,2043],{}," PK auto \u002F ",[32,6248,6249],{},"company_id"," FK→",[32,6252,1064],{},[32,6254,6255],{},"title",[32,6257,6258],{},"version",[32,6260,6261],{},"contract_state"," ('initial'\u002F'renew'\u002F'done'\u002F'expired', 기본 'initial') \u002F ",[32,6264,6265],{},"status",[32,6267,6268],{},"signer_user_id",[32,6270,1058],{},[32,6272,6273],{},"signed_at",[32,6275,2092],{},[32,6277,2095],{},[32,6279,6280],{},"updated_at",". 인덱스 ",[32,6283,6284],{},"(company_id, contract_state)",[241,6286,6287,512,6290,6246,6292,6250,6295,1225,6297,6299,6300,1225,6303,1225,6306,6281,6309],{},[32,6288,6289],{},"contractFile",[32,6291,2043],{},[32,6293,6294],{},"contract_id",[32,6296,6017],{},[32,6298,1992],{}," (한국어 접두사 포함) \u002F ",[32,6301,6302],{},"size_bytes",[32,6304,6305],{},"r2_key",[32,6307,6308],{},"uploaded_at",[32,6310,6311],{},"(contract_id)",[14,6313,6315],{"id":6314},"_114-signup-자동-initial-계약-생성","11.4 signup 자동 'initial' 계약 생성",[19,6317,6318,6320],{},[255,6319,502],{"href":1138}," — NICE 세션 consume 직후 분기:",[1011,6322,6324],{"className":1658,"code":6323,"language":1660,"meta":1016,"style":1016},"if (body.companyType === 'corp' || body.companyType === 'sole') {\n  await db.insert(contract).values({\n    companyId,\n    title: '최초 이용계약 온라인체결',\n    version: '신규',\n    contractState: 'initial',\n    status: 1,\n  })\n}\n",[32,6325,6326,6351,6370,6375,6386,6396,6406,6415,6420],{"__ignoreMap":1016},[1020,6327,6328,6330,6333,6335,6338,6341,6344,6346,6349],{"class":1022,"line":1023},[1020,6329,1742],{"class":1667},[1020,6331,6332],{"class":1681}," (body.companyType ",[1020,6334,1685],{"class":1667},[1020,6336,6337],{"class":1688}," 'corp'",[1020,6339,6340],{"class":1667}," ||",[1020,6342,6343],{"class":1681}," body.companyType ",[1020,6345,1685],{"class":1667},[1020,6347,6348],{"class":1688}," 'sole'",[1020,6350,4581],{"class":1681},[1020,6352,6353,6356,6358,6361,6364,6367],{"class":1022,"line":1029},[1020,6354,6355],{"class":1667},"  await",[1020,6357,4482],{"class":1681},[1020,6359,6360],{"class":1748},"insert",[1020,6362,6363],{"class":1681},"(contract).",[1020,6365,6366],{"class":1748},"values",[1020,6368,6369],{"class":1681},"({\n",[1020,6371,6372],{"class":1022,"line":1035},[1020,6373,6374],{"class":1681},"    companyId,\n",[1020,6376,6377,6380,6383],{"class":1022,"line":1739},[1020,6378,6379],{"class":1681},"    title: ",[1020,6381,6382],{"class":1688},"'최초 이용계약 온라인체결'",[1020,6384,6385],{"class":1681},",\n",[1020,6387,6388,6391,6394],{"class":1022,"line":1764},[1020,6389,6390],{"class":1681},"    version: ",[1020,6392,6393],{"class":1688},"'신규'",[1020,6395,6385],{"class":1681},[1020,6397,6398,6401,6404],{"class":1022,"line":1769},[1020,6399,6400],{"class":1681},"    contractState: ",[1020,6402,6403],{"class":1688},"'initial'",[1020,6405,6385],{"class":1681},[1020,6407,6408,6411,6413],{"class":1022,"line":1775},[1020,6409,6410],{"class":1681},"    status: ",[1020,6412,696],{"class":1671},[1020,6414,6385],{"class":1681},[1020,6416,6417],{"class":1022,"line":1794},[1020,6418,6419],{"class":1681},"  })\n",[1020,6421,6422],{"class":1022,"line":1799},[1020,6423,3017],{"class":1681},[19,6425,6426,6428,6429,6431],{},[32,6427,3682],{},"은 자동 생성 안 함 — ",[32,6430,5638],{},"에 진입할 일이 없음(§10 정책).",[14,6433,6435,6436,6438],{"id":6434},"_115-contracts-라우트-5-엔드포인트","11.5 ",[32,6437,6050],{}," 라우트 — 5 엔드포인트",[19,6440,6441,6445],{},[255,6442,6444],{"href":6443},"..\u002F..\u002F..\u002Fmalgn-noti-api\u002Fsrc\u002Froutes\u002Fcontracts.ts","src\u002Froutes\u002Fcontracts.ts"," 신규(244 줄):",[143,6447,6448,6456],{},[146,6449,6450],{},[149,6451,6452,6454],{},[152,6453,2262],{},[152,6455,2265],{},[165,6457,6458,6468,6491,6504,6514,6531],{},[149,6459,6460,6465],{},[170,6461,6462],{},[32,6463,6464],{},"GET \u002Fcontracts",[170,6466,6467],{},"본 회사 계약 목록 (status=1 한정, id 오름차순)",[149,6469,6470,6475],{},[170,6471,6472],{},[32,6473,6474],{},"POST \u002Fcontracts\u002F:id\u002Fsign",[170,6476,6477,6478,122,6481,122,6484,122,6487,6490],{},"'initial' 또는 'renew' → ",[32,6479,6480],{},"'done'",[32,6482,6483],{},"signer_user_id=ctx.userId",[32,6485,6486],{},"signed_at=now",[32,6488,6489],{},"expires_at=+2y",". renew였다면 같은 회사의 다른 done 계약 모두 'expired'로 일괄 전이",[149,6492,6493,6498],{},[170,6494,6495],{},[32,6496,6497],{},"GET \u002Fcontracts\u002Ffiles",[170,6499,6500,6501,6503],{},"본 회사 파일 목록 (",[32,6502,6243],{}," JOIN으로 회사 단위 좁힘)",[149,6505,6506,6511],{},[170,6507,6508],{},[32,6509,6510],{},"POST \u002Fcontracts\u002Ffiles",[170,6512,6513],{},"multipart (contractId \u002F kind \u002F file). PDF·10MB 검증 + 회사 소유 검증 → R2 put + DB insert",[149,6515,6516,6521],{},[170,6517,6518],{},[32,6519,6520],{},"GET \u002Fcontracts\u002Ffiles\u002F:id\u002Fdownload",[170,6522,6523,6524,6527,6528,1488],{},"R2 stream → ",[32,6525,6526],{},"application\u002Fpdf"," 응답 (",[32,6529,6530],{},"Content-Disposition: inline; filename*=UTF-8''…",[149,6532,6533,6538],{},[170,6534,6535],{},[32,6536,6537],{},"DELETE \u002Fcontracts\u002Ffiles\u002F:id",[170,6539,6540],{},"R2 delete(실패 swallow) + DB delete",[19,6542,6543,6544,6547,6548,6550,6551,6554],{},"라우트는 ",[32,6545,6546],{},"app.use('*', requireAuth())","만 — 승인 게이트(",[32,6549,4864],{},")는 ",[23,6552,6553],{},"적용 안 함"," (미승인 사용자가 사업자등록증을 업로드해야 하기 때문).",[19,6556,6557,6558,6561],{},"R2 key 패턴: ",[32,6559,6560],{},"contracts\u002F\u003CcompanyId>\u002F\u003CcontractId>\u002F\u003Cunix>_\u003CsafeName>"," — 회사·계약별로 prefix 분리 + 안전한 ASCII 파일명으로 escape.",[14,6563,6565],{"id":6564},"_116-openapi","11.6 OpenAPI",[238,6567,6568,6589,6598],{},[241,6569,6570,6571,6573,6574,6577,6578,6581,6582,6573,6585,6588],{},"5 paths 추가 (",[32,6572,6050],{}," GET, ",[32,6575,6576],{},"\u002Fcontracts\u002F{id}\u002Fsign"," POST, ",[32,6579,6580],{},"\u002Fcontracts\u002Ffiles"," GET·POST, ",[32,6583,6584],{},"\u002Fcontracts\u002Ffiles\u002F{id}\u002Fdownload",[32,6586,6587],{},"\u002Fcontracts\u002Ffiles\u002F{id}"," DELETE)",[241,6590,6591,6592,581,6595,1488],{},"2 schemas 추가 (",[32,6593,6594],{},"Contract",[32,6596,6597],{},"ContractFile",[241,6599,6600,6601,6604,6605,6608],{},"목록은 ",[32,6602,6603],{},"cursorList()"," 헬퍼 재사용(실제로는 cursor 없음, 응답 형식은 ",[32,6606,6607],{},"{data:[...]}"," 동일)",[14,6610,6612,6613,6615],{"id":6611},"_117-프런트-appcontractpanelvue-실-api-연동","11.7 프런트 — ",[32,6614,6075],{}," 실 API 연동",[19,6617,6618,6619,54,6622,54,6625,54,6628,6631],{},"목업 데이터(",[32,6620,6621],{},"contracts[3건]",[32,6623,6624],{},"bizFiles[2건]",[32,6626,6627],{},"nowStamp()",[32,6629,6630],{},"expiryStamp()",") 모두 제거. 핵심 변경:",[238,6633,6634,6642,6679,6689,6702,6711,6723,6737],{},[241,6635,6636,560,6639,6641],{},[23,6637,6638],{},"로딩",[32,6640,6079],{}," — top-level await (Nuxt SSR 호환)",[241,6643,6644,6647,6648,6651,6652,54,6655,6658,6659,6662,6663,6666,6667,6670,6671,6674,6675,6678],{},[23,6645,6646],{},"상태 매핑",": 백엔드 ",[32,6649,6650],{},"contractState","를 STATE_META 테이블로 ",[32,6653,6654],{},"statusLabel",[32,6656,6657],{},"icon"," 매핑 + ",[32,6660,6661],{},"metas"," 자동 구성(",[32,6664,6665],{},"initial","은 가입 안내·요청일, ",[32,6668,6669],{},"done","은 체결·만료, ",[32,6672,6673],{},"renew","는 요청일, ",[32,6676,6677],{},"expired","는 만료)",[241,6680,6681,6684,6685,6688],{},[23,6682,6683],{},"파일 분류",": 목록은 단일 호출 → ",[32,6686,6687],{},"classify(name)"," 으로 biz\u002Floan\u002Finsurance 그룹 분배 → 표시명은 접두사 제거",[241,6690,6691,6694,6695,54,6698,6701],{},[23,6692,6693],{},"첨부 활성화",": 파일이 이미 있으면 ",[32,6696,6697],{},"loanApplicable",[32,6699,6700],{},"insuranceApplicable"," 자동 true",[241,6703,6704,560,6707,6710],{},[23,6705,6706],{},"활성 계약 결정",[32,6708,6709],{},"activeContractId = initial || renew || 가장 오래된"," — 새 첨부는 이쪽으로 묶음",[241,6712,6713,560,6716,6719,6720,6722],{},[23,6714,6715],{},"업로드",[32,6717,6718],{},"FormData"," POST ",[32,6721,6580],{},". 실패 시 토스트 + input.value 초기화",[241,6724,6725,560,6728,1516,6730,6733,6734,6736],{},[23,6726,6727],{},"체결",[32,6729,6474],{},[32,6731,6732],{},"loadContracts()"," 재호출 → 토스트(",[32,6735,6673],{},"였다면 \"기존 계약 만료 처리\" 메시지)",[241,6738,6739,6742,6743,1516,6746,1516,6749,1480,6752,6755,6756],{},[23,6740,6741],{},"미리보기",": iframe은 Authorization 헤더를 못 싣기 때문에 ",[32,6744,6745],{},"api\u003CBlob>('\u002Fcontracts\u002Ffiles\u002F:id\u002Fdownload', { responseType: 'blob' })",[32,6747,6748],{},"URL.createObjectURL",[32,6750,6751],{},"AppFilePreviewDialog",[32,6753,6754],{},"file.url"," 전달. modal 닫힐 때 ",[32,6757,6758],{},"revokeObjectURL",[14,6760,6762],{"id":6761},"_118-라이브-e2e-검증-4-시나리오","11.8 라이브 e2e 검증 (4 시나리오)",[143,6764,6765,6775],{},[146,6766,6767],{},[149,6768,6769,6771,6773],{},[152,6770,683],{},[152,6772,3422],{},[152,6774,689],{},[165,6776,6777,6793,6808,6819],{},[149,6778,6779,6781,6784],{},[170,6780,696],{},[170,6782,6783],{},"corp signup (mock NICE)",[170,6785,6786,6787,6789,6790,6792],{},"✅ 자동 ",[32,6788,6017],{}," 1건 'initial' 생성 (",[32,6791,6050],{}," 응답 정상)",[149,6794,6795,6797,6802],{},[170,6796,707],{},[170,6798,6799,6801],{},[32,6800,6510],{}," (105B PDF, kind=biz)",[170,6803,6804,6805],{},"✅ 201 + ",[32,6806,6807],{},"name='사업자등록증_test.pdf'",[149,6809,6810,6812,6816],{},[170,6811,717],{},[170,6813,6814],{},[32,6815,6497],{},[170,6817,6818],{},"✅ 1건 목록",[149,6820,6821,6823,6827],{},[170,6822,729],{},[170,6824,6825],{},[32,6826,6474],{},[170,6828,6829,6830,1516,6833,1516,6835,581,6838,2071,6841,6844],{},"✅ ",[32,6831,6832],{},"data.ok:true",[32,6834,6464],{},[32,6836,6837],{},"contractState='done'",[32,6839,6840],{},"signedAt",[32,6842,6843],{},"expiresAt","(+2y) 정확",[19,6846,6847],{},"E2E 데이터 cleanup 완료 (TB_CONTRACT_FILE 0 \u002F TB_CONTRACT 0 \u002F TB_USER e2e_% 0 \u002F TB_COMPANY E2E% 0 \u002F R2 객체 1건 삭제).",[14,6849,6851],{"id":6850},"_119-산출물","11.9 산출물",[238,6853,6854,6883,6891],{},[241,6855,2646,6856,6859,6860,6862,6863,54,6865,54,6867,54,6869,54,6871,54,6873,6876,6877,6879,6880,6882],{},[32,6857,6858],{},"malgn-noti-api"," — 신규 ",[32,6861,6444],{},"(244) · 수정 ",[32,6864,6008],{},[32,6866,6206],{},[32,6868,783],{},[32,6870,502],{},[32,6872,779],{},[32,6874,6875],{},"src\u002Findex.ts",". Workers 배포 #17 Version ",[32,6878,6083],{},". R2 bucket ",[32,6881,6004],{}," 신규.",[241,6884,3520,6885,6888,6889,806],{},[32,6886,6887],{},"app\u002Fcomponents\u002FAppContractPanel.vue"," 전면 교체. Pages 배포 alias ",[32,6890,6087],{},[241,6892,6893,6894,6896],{},"WBS: 5-3C-* 신규 항목 \"계약 관리 (",[32,6895,5638],{},") 실 API 연동\" — 사업자등록증 업로드 + 계약 체결 ✅",[14,6898,6900],{"id":6899},"_1110-알려진-한계-다음-작업","11.10 알려진 한계 \u002F 다음 작업",[238,6902,6903,6918,6928,6933,6939],{},[241,6904,6905,6910,6911,54,6914,6917],{},[23,6906,6907,6909],{},[32,6908,5823],{}," 헤더의 안내문구","가 §10에서 지적된 \"미승인 \u002F 반려에 따른 분기\"는 아직 미반영. 모달 UX(",[32,6912,6913],{},"AppUploadGuideDialog",[32,6915,6916],{},"AppContractSignDialog",")도 현 상태 유지.",[241,6919,6920,6923,6924,6927],{},[23,6921,6922],{},"운영자단 사업자 승인 화면 미구현"," (P0 §7.7) — 현재 라이브 DB UPDATE만으로 승인\u002F반려. 운영자단 라우트(",[32,6925,6926],{},"\u002Fadmin\u002F...",") 신설 필요.",[241,6929,6930,6932],{},[23,6931,4188],{}," (승인\u002F반려\u002F계약 만료) — NHN 자격증명 등록 후 trigger.",[241,6934,6935,6938],{},[23,6936,6937],{},"사업자등록증 OCR 자동 검증"," — 향후 운영자 부담 경감 검토. 현재는 운영자 수동 심사.",[241,6940,6941,6944,6945,6947],{},[23,6942,6943],{},"계약 갱신 트리거"," — 만료 1개월 전 자동 'renew' 계약 row 생성 cron 필요(아직 없음). signed_at + 2y가 가까워지면 ",[32,6946,6843],{},"만으로 판단해 화면에서 경고는 가능.",[94,6949],{},[10,6951,6953],{"id":6952},"_12-사업자등록증-첨부-시-회사-승인-상태-reviewing-자동-전이-배포-1819-pages-alias-d9a82bfa","§12. 사업자등록증 첨부 시 회사 승인 상태 'reviewing' 자동 전이 (배포 #18·#19 \u002F Pages alias d9a82bfa)",[14,6955,103],{"id":6956},"한-줄-11",[19,6958,6959,6960,6963,6964,6969,6970,6973,6974,6977,6978,6980,6981,476,6983,6986,6987,520,6989,5746,6991,6993,6994,6997,6998,30,7000,7002,7003,7005,7006,7008,7009,7011,7012,7015,7016,7019,7020,54,7022,7025,7026,7028,7029,30,7031,7034,7035,7037,7038,7041,7042,6084,7045,7048],{},"§11의 후속 — 사용자가 \"사업자등록증을 첨부하면 심사 중으로 변경해 달라\"고 요청. ",[32,6961,6962],{},"approval_state"," enum에 ",[23,6965,6966],{},[32,6967,6968],{},"reviewing"," 추가(",[32,6971,6972],{},"pending → reviewing → approved | rejected"," 4단계). DDL 변경은 없음(",[32,6975,6976],{},"VARCHAR(20)","이라 자동 흡수). ",[23,6979,6000],{}," 백엔드 ",[32,6982,6510],{},[32,6984,6985],{},"kind=biz"," 업로드 후 회사 상태가 ",[32,6988,2288],{},[32,6990,3707],{},[32,6992,6968],{},"으로 UPDATE. ",[32,6995,6996],{},"rejected_reason","은 그대로 둠(운영자가 결정). ",[23,6999,6030],{},[32,7001,4864],{}," 미들웨어 + ",[32,7004,4096],{}," PATCH 차단 메시지에 ",[32,7007,6968],{}," 분기 추가(\"사업자등록증 심사가 진행 중입니다. 승인 완료 후 …\"). ",[23,7010,6047],{}," 사용자단 ",[32,7013,7014],{},"AuthCompany.approvalState"," 타입 확장(",[32,7017,7018],{},"'reviewing'"," 추가) + ",[32,7021,5037],{},[32,7023,7024],{},"AppMemberInfoPanel"," 안내문구 3분기 + ",[32,7027,5823],{}," 패널 상단에 승인 상태 카드(pending=warning · reviewing=info · rejected=danger) 신규. ",[23,7030,6067],{},[32,7032,7033],{},"pickFile"," 성공 후 ",[32,7036,6985],{},"면 ",[32,7039,7040],{},"auth.fetchMe()"," 호출 → store 즉시 갱신 → 글로벌 띠·페이지 배너가 즉시 \"심사 중\"으로 전환(새로고침 불필요). Workers 배포 #19(Version ",[32,7043,7044],{},"f6877b91-4b2c-429e-951f-9185bcf69c4a",[32,7046,7047],{},"d9a82bfa.malgn-noti.pages.dev",". 라이브 e2e 통과(corp 가입 직후 pending → biz 업로드 → reviewing 자동 전이 + PATCH \u002Fme 시도 시 새 메시지 노출).",[14,7050,7052,7053,7055],{"id":7051},"_121-정책-approval_state-4단계로-확장","12.1 정책 — ",[32,7054,6962],{}," 4단계로 확장",[143,7057,7058,7073],{},[146,7059,7060],{},[149,7061,7062,7065,7067,7070],{},[152,7063,7064],{},"상태",[152,7066,160],{},[152,7068,7069],{},"진입 트리거",[152,7071,7072],{},"사용자 액션",[165,7074,7075,7090,7105,7120],{},[149,7076,7077,7081,7084,7087],{},[170,7078,7079],{},[32,7080,2288],{},[170,7082,7083],{},"사업자등록증 미제출",[170,7085,7086],{},"corp\u002Fsole signup",[170,7088,7089],{},"계약 관리에서 사업자등록증 업로드",[149,7091,7092,7096,7099,7102],{},[170,7093,7094],{},[32,7095,6968],{},[170,7097,7098],{},"운영자 심사 대기",[170,7100,7101],{},"biz 첨부 완료 시 자동(pending\u002Frejected에서만)",[170,7103,7104],{},"대기 — 결과 안내 메일\u002FSMS",[149,7106,7107,7111,7114,7117],{},[170,7108,7109],{},[32,7110,3701],{},[170,7112,7113],{},"승인",[170,7115,7116],{},"운영자(BackOffice)",[170,7118,7119],{},"모든 기능 이용",[149,7121,7122,7126,7129,7132],{},[170,7123,7124],{},[32,7125,3707],{},[170,7127,7128],{},"반려",[170,7130,7131],{},"운영자(BackOffice) — 사유 입력",[170,7133,7134],{},"사업자등록증 재첨부 → reviewing으로 자동 전이",[238,7136,7137,7145,7157],{},[241,7138,7139,7141,7142,7144],{},[32,7140,3682],{},"은 처음부터 ",[32,7143,3701],{}," — 영향 없음",[241,7146,7147,7149,7150,7153,7154,7156],{},[32,7148,4864],{}," 미들웨어는 여전히 ",[32,7151,7152],{},"state !== 'approved'"," 차단 — ",[32,7155,6968],{},"도 차단 대상(메시지만 분기)",[241,7158,7159,7160,7163,7164,7167],{},"DDL 변경 없음 (",[32,7161,7162],{},"approval_state VARCHAR(20)","). 추후 ",[32,7165,7166],{},"idx_company_approval","은 4 상태 모두 같은 컬럼이라 그대로 효율적",[14,7169,7171],{"id":7170},"_122-백엔드-상태-전이-메시지","12.2 백엔드 — 상태 전이 + 메시지",[1040,7173,7175,7176,7178],{"id":7174},"post-contractsfiles-kindbiz만-발동","POST \u002Fcontracts\u002Ffiles (",[32,7177,6985],{},"만 발동)",[1011,7180,7182],{"className":1658,"code":7181,"language":1660,"meta":1016,"style":1016},"if (kind === 'biz') {\n  const cs = await db.select({ state: company.approvalState })...\n  if (cs[0]?.state === 'pending' || cs[0]?.state === 'rejected') {\n    await db.update(company).set({ approvalState: 'reviewing' })\n      .where(eq(company.id, companyId))\n  }\n}\n",[32,7183,7184,7198,7219,7252,7274,7288,7292],{"__ignoreMap":1016},[1020,7185,7186,7188,7191,7193,7196],{"class":1022,"line":1023},[1020,7187,1742],{"class":1667},[1020,7189,7190],{"class":1681}," (kind ",[1020,7192,1685],{"class":1667},[1020,7194,7195],{"class":1688}," 'biz'",[1020,7197,4581],{"class":1681},[1020,7199,7200,7202,7205,7207,7209,7211,7213,7216],{"class":1022,"line":1029},[1020,7201,4355],{"class":1667},[1020,7203,7204],{"class":1671}," cs",[1020,7206,1675],{"class":1667},[1020,7208,3390],{"class":1667},[1020,7210,4482],{"class":1681},[1020,7212,4485],{"class":1748},[1020,7214,7215],{"class":1681},"({ state: company.approvalState })",[1020,7217,7218],{"class":1667},"...\n",[1020,7220,7221,7223,7226,7228,7231,7233,7236,7238,7241,7243,7245,7247,7250],{"class":1022,"line":1035},[1020,7222,5262],{"class":1667},[1020,7224,7225],{"class":1681}," (cs[",[1020,7227,4535],{"class":1671},[1020,7229,7230],{"class":1681},"]?.state ",[1020,7232,1685],{"class":1667},[1020,7234,7235],{"class":1688}," 'pending'",[1020,7237,6340],{"class":1667},[1020,7239,7240],{"class":1681}," cs[",[1020,7242,4535],{"class":1671},[1020,7244,7230],{"class":1681},[1020,7246,1685],{"class":1667},[1020,7248,7249],{"class":1688}," 'rejected'",[1020,7251,4581],{"class":1681},[1020,7253,7254,7256,7258,7261,7263,7266,7269,7271],{"class":1022,"line":1739},[1020,7255,4611],{"class":1667},[1020,7257,4482],{"class":1681},[1020,7259,7260],{"class":1748},"update",[1020,7262,4499],{"class":1681},[1020,7264,7265],{"class":1748},"set",[1020,7267,7268],{"class":1681},"({ approvalState: ",[1020,7270,7018],{"class":1688},[1020,7272,7273],{"class":1681}," })\n",[1020,7275,7276,7279,7281,7283,7285],{"class":1022,"line":1764},[1020,7277,7278],{"class":1681},"      .",[1020,7280,4502],{"class":1748},[1020,7282,1752],{"class":1681},[1020,7284,4507],{"class":1748},[1020,7286,7287],{"class":1681},"(company.id, companyId))\n",[1020,7289,7290],{"class":1022,"line":1769},[1020,7291,4621],{"class":1681},[1020,7293,7294],{"class":1022,"line":1775},[1020,7295,3017],{"class":1681},[238,7297,7298,7304,7311,7320],{},[241,7299,7300,7301],{},"첫 첨부 시: ",[32,7302,7303],{},"pending → reviewing",[241,7305,7306,7307,7310],{},"반려 후 재첨부 시: ",[32,7308,7309],{},"rejected → reviewing"," (사유는 그대로 둠 → 운영자가 새 심사에서 덮어쓰거나 NULL로 설정)",[241,7312,7313,7314,7316,7317,7319],{},"이미 ",[32,7315,6968],{},"이거나 ",[32,7318,3701],{},"면 변동 없음(idempotent)",[241,7321,7322,1225,7325,7328],{},[32,7323,7324],{},"kind=loan",[32,7326,7327],{},"kind=insurance","는 트리거 안 함 — 보조 서류",[1040,7330,7332],{"id":7331},"메시지-분기-2-곳","메시지 분기 — 2 곳",[19,7334,7335,503],{},[255,7336,4239],{"href":4244},[1011,7338,7340],{"className":1658,"code":7339,"language":1660,"meta":1016,"style":1016},"state === 'rejected' ? '심사가 반려되어 이용할 수 없습니다. 사유: …'\n: state === 'reviewing' ? '사업자등록증 심사가 진행 중입니다. 승인 완료 후 이용할 수 있습니다.'\n: '사업자등록증을 등록한 후 이용할 수 있습니다.'\n",[32,7341,7342,7355,7371],{"__ignoreMap":1016},[1020,7343,7344,7346,7348,7350,7352],{"class":1022,"line":1023},[1020,7345,5300],{"class":1681},[1020,7347,1685],{"class":1667},[1020,7349,7249],{"class":1688},[1020,7351,1692],{"class":1667},[1020,7353,7354],{"class":1688}," '심사가 반려되어 이용할 수 없습니다. 사유: …'\n",[1020,7356,7357,7359,7361,7363,7366,7368],{"class":1022,"line":1029},[1020,7358,503],{"class":1667},[1020,7360,5306],{"class":1681},[1020,7362,1685],{"class":1667},[1020,7364,7365],{"class":1688}," 'reviewing'",[1020,7367,1692],{"class":1667},[1020,7369,7370],{"class":1688}," '사업자등록증 심사가 진행 중입니다. 승인 완료 후 이용할 수 있습니다.'\n",[1020,7372,7373,7375],{"class":1022,"line":1035},[1020,7374,503],{"class":1667},[1020,7376,7377],{"class":1688}," '사업자등록증을 등록한 후 이용할 수 있습니다.'\n",[19,7379,7380,512,7382,54,7384,7386],{},[255,7381,2834],{"href":2833},[32,7383,2782],{},[32,7385,2785],{}," 두 핸들러 모두 동일 분기 추가(\"정보를 수정할 수 있습니다\" 변형).",[14,7388,7390],{"id":7389},"_123-사용자단-타입배너카드store-갱신","12.3 사용자단 — 타입·배너·카드·store 갱신",[1040,7392,7394],{"id":7393},"타입-확장","타입 확장",[19,7396,7397,512,7399,7401,7402,7404],{},[255,7398,790],{"href":1197},[32,7400,7014],{}," 에 ",[32,7403,7018],{}," 추가. 타입 좁힘이 풀려 모든 화면의 컴파일 에러가 한 번에 해소됨.",[1040,7406,7408],{"id":7407},"appapprovalbanner-글로벌-띠","AppApprovalBanner — 글로벌 띠",[19,7410,7411,512,7413,7416,7417,7419],{},[255,7412,5074],{"href":5073},[32,7414,7415],{},"visible","을 ",[32,7418,7152],{},"로 단순화(이전엔 pending\u002Frejected만 명시). 본문 3분기:",[238,7421,7422,7425,7428],{},[241,7423,7424],{},"pending: \"사업자등록증을 등록해 주세요\"",[241,7426,7427],{},"reviewing: \"사업자등록증 심사 중입니다 — 영업일 1~2일 내 안내\"",[241,7429,7430],{},"rejected: \"사업자등록증 심사 반려 — 사유: …\"",[19,7432,7433,7434,7437,7438,7441],{},"CTA 라벨: rejected=\"다시 제출하기\" \u002F reviewing=\"진행 상태 보기\" \u002F pending=\"사업자등록증 등록\". 클래스는 ",[32,7435,7436],{},":class=\"state\"","로 단순화(",[32,7439,7440],{},"approval-banner.reviewing","은 별도 톤 없이 pending과 같은 warning 톤 유지).",[1040,7443,7445],{"id":7444},"appmemberinfopanel-페이지-배너","AppMemberInfoPanel — 페이지 배너",[19,7447,7448,7450,7451,806],{},[255,7449,3526],{"href":2807}," — 같은 패턴(3분기 strong + p). 클래스는 ",[32,7452,7453],{},":class=\"approvalState\"",[1040,7455,7457],{"id":7456},"appcontractpanel-패널-상단-상태-카드","AppContractPanel — 패널 상단 상태 카드",[19,7459,7460,512,7463,7466,7467,7469],{},[255,7461,6887],{"href":7462},"..\u002F..\u002Fapp\u002Fcomponents\u002FAppContractPanel.vue",[32,7464,7465],{},"\u003Cdiv class=\"state-card\" :class=\"approvalState\">"," 신규(",[32,7468,3925],{}," 화면에서만 노출):",[238,7471,7472,7475,7478],{},[241,7473,7474],{},"pending: warning 톤 + 시계 아이콘 + \"사업자등록증을 등록해 주세요\" + \"PDF, 최대 10MB\" 안내",[241,7476,7477],{},"reviewing: info 톤 + 로딩 아이콘 + \"사업자등록증 심사 중입니다\" + 영업일 안내",[241,7479,7480],{},"rejected: danger 톤 + X 아이콘 + 반려 사유 + \"다시 첨부 시 재심사\" 안내",[19,7482,7483,7484,54,7487,54,7490,130],{},"CSS 토큰만 사용(",[32,7485,7486],{},"--warning-line",[32,7488,7489],{},"--info-soft",[32,7491,7492],{},"--danger",[1040,7494,7496],{"id":7495},"pickfile-성공-후-store-즉시-갱신","pickFile 성공 후 store 즉시 갱신",[1011,7498,7500],{"className":1658,"code":7499,"language":1660,"meta":1016,"style":1016},"await api('\u002Fcontracts\u002Ffiles', { method: 'POST', body: form })\nawait loadFiles()\nif (target === 'biz') await auth.fetchMe()\ntoast.add({ title: target === 'biz' && approvalState.value === 'reviewing'\n  ? '사업자등록증이 제출되었습니다. 심사가 진행됩니다.'\n  : '서류가 첨부되었습니다.', ... })\n",[32,7501,7502,7523,7532,7555,7580,7588],{"__ignoreMap":1016},[1020,7503,7504,7507,7510,7512,7515,7518,7520],{"class":1022,"line":1023},[1020,7505,7506],{"class":1667},"await",[1020,7508,7509],{"class":1748}," api",[1020,7511,1752],{"class":1681},[1020,7513,7514],{"class":1688},"'\u002Fcontracts\u002Ffiles'",[1020,7516,7517],{"class":1681},", { method: ",[1020,7519,4271],{"class":1688},[1020,7521,7522],{"class":1681},", body: form })\n",[1020,7524,7525,7527,7530],{"class":1022,"line":1029},[1020,7526,7506],{"class":1667},[1020,7528,7529],{"class":1748}," loadFiles",[1020,7531,4432],{"class":1681},[1020,7533,7534,7536,7539,7541,7543,7545,7547,7550,7553],{"class":1022,"line":1035},[1020,7535,1742],{"class":1667},[1020,7537,7538],{"class":1681}," (target ",[1020,7540,1685],{"class":1667},[1020,7542,7195],{"class":1688},[1020,7544,3376],{"class":1681},[1020,7546,7506],{"class":1667},[1020,7548,7549],{"class":1681}," auth.",[1020,7551,7552],{"class":1748},"fetchMe",[1020,7554,4432],{"class":1681},[1020,7556,7557,7560,7563,7566,7568,7570,7572,7575,7577],{"class":1022,"line":1739},[1020,7558,7559],{"class":1681},"toast.",[1020,7561,7562],{"class":1748},"add",[1020,7564,7565],{"class":1681},"({ title: target ",[1020,7567,1685],{"class":1667},[1020,7569,7195],{"class":1688},[1020,7571,4409],{"class":1667},[1020,7573,7574],{"class":1681}," approvalState.value ",[1020,7576,1685],{"class":1667},[1020,7578,7579],{"class":1688}," 'reviewing'\n",[1020,7581,7582,7585],{"class":1022,"line":1764},[1020,7583,7584],{"class":1667},"  ?",[1020,7586,7587],{"class":1688}," '사업자등록증이 제출되었습니다. 심사가 진행됩니다.'\n",[1020,7589,7590,7593,7596,7598,7600],{"class":1022,"line":1769},[1020,7591,7592],{"class":1667},"  :",[1020,7594,7595],{"class":1688}," '서류가 첨부되었습니다.'",[1020,7597,581],{"class":1681},[1020,7599,2881],{"class":1667},[1020,7601,7273],{"class":1681},[19,7603,7604,7606,7607,7609,7610,7612,7613,7615],{},[32,7605,5465],{},"로 store가 갱신되면 — 글로벌 띠(",[32,7608,5037],{},")·페이지 배너(",[32,7611,7024],{}," 진입 시)·이 패널 상태 카드(",[32,7614,3792],{}," computed) 모두 같은 store를 구독하므로 즉시 \"심사 중\"으로 전환됨.",[14,7617,7619],{"id":7618},"_124-라이브-e2e-검증","12.4 라이브 e2e 검증",[143,7621,7622,7632],{},[146,7623,7624],{},[149,7625,7626,7628,7630],{},[152,7627,683],{},[152,7629,3422],{},[152,7631,689],{},[165,7633,7634,7646,7659,7671,7684],{},[149,7635,7636,7638,7640],{},[170,7637,696],{},[170,7639,6783],{},[170,7641,6829,7642,1516,7644],{},[32,7643,249],{},[32,7645,3779],{},[149,7647,7648,7650,7654],{},[170,7649,707],{},[170,7651,7652],{},[32,7653,6464],{},[170,7655,6829,7656,7658],{},[32,7657,6403],{}," 1건 자동 생성됨",[149,7660,7661,7663,7668],{},[170,7662,717],{},[170,7664,7665,7667],{},[32,7666,6510],{}," (kind=biz)",[170,7669,7670],{},"✅ 201",[149,7672,7673,7675,7679],{},[170,7674,729],{},[170,7676,7677,3472],{},[32,7678,3435],{},[170,7680,6829,7681],{},[32,7682,7683],{},"approvalState='reviewing'",[149,7685,7686,7688,7692],{},[170,7687,739],{},[170,7689,7690],{},[32,7691,3989],{},[170,7693,7694],{},"✅ 403 \"사업자등록증 심사가 진행 중입니다. 승인 완료 후 정보를 수정할 수 있습니다.\"",[19,7696,7697],{},"테스트 데이터 cleanup(R2 객체 1 + DB rows) 완료.",[14,7699,7701],{"id":7700},"_125-산출물","12.5 산출물",[238,7703,7704,7722],{},[241,7705,2646,7706,7709,7710,54,7713,54,7716,7719,7720],{},[32,7707,7708],{},"malgn-noti-api: 66dab21"," — 3 파일 수정(",[32,7711,7712],{},"routes\u002Fcontracts.ts",[32,7714,7715],{},"middleware\u002Fapproval.ts",[32,7717,7718],{},"routes\u002Fme.ts","). Workers 배포 Version ",[32,7721,7044],{},[241,7723,3520,7724,7727,7728,54,7730,54,7732,54,7734,7736,7737],{},[32,7725,7726],{},"malgn-noti: 5d530d9"," — 4 파일 수정(",[32,7729,5068],{},[32,7731,2808],{},[32,7733,6075],{},[32,7735,2460],{},"). Pages 배포 alias ",[32,7738,7047],{},[14,7740,7742],{"id":7741},"_126-알려진-한계-후속-작업","12.6 알려진 한계 \u002F 후속 작업",[238,7744,7745,7757,7765,7780],{},[241,7746,7747,7750,7751,7753,7754,7756],{},[23,7748,7749],{},"운영자단 심사 화면 미구현"," (§7.7부터 누적) — ",[32,7752,6968],{},"까지 자동 진행되지만 승인\u002F반려는 여전히 DB 직접 UPDATE. 운영자단 BackOffice 화면(",[32,7755,6926],{},") 필요.",[241,7758,7759,512,7762,7764],{},[23,7760,7761],{},"알림 미발송",[32,7763,7303],{}," 전이 시 사용자에게 알림 메일\u002FSMS 없음. NHN 자격증명 발급 후 trigger.",[241,7766,7767,4141,7770,7772,7773,7775,7776,7779],{},[23,7768,7769],{},"반려 후 재첨부 시 사유 처리",[32,7771,6996],{},"을 그대로 둔 채 ",[32,7774,6968],{},"으로 전이. 운영자가 새 심사에서 결정하도록 위임. 정책상 더 명확히 하려면 재첨부 시 ",[32,7777,7778],{},"rejected_reason=NULL","로 정리도 가능 — 향후 결정.",[241,7781,7782,7785],{},[23,7783,7784],{},"사업자등록증 외 보조 서류 정책"," — 대부업등록증·보험증권은 첨부해도 상태 전이 없음. 운영자가 별도로 확인. 후속 정책 검토 시 변경 가능.",[94,7787],{},[10,7789,7791],{"id":7790},"_13-accountcontract-첫-진입-회복-lazy-auto-create-reviewing-자동-회복-배포-18-pages-6a7a7d2ca657b2","§13. \u002Faccount\u002Fcontract 첫 진입 회복 — lazy auto-create + reviewing 자동 회복 (배포 #18 \u002F Pages 6a7a7d2·ca657b2)",[14,7793,103],{"id":7794},"한-줄-12",[19,7796,7797,7798,7801,7802,7804,7805,7807,7808,7811,7812,7814,7815,7818,7819,7822,7823,7826,7827,7830,7831,7833,7834,7836,7837,7839,7840,7842,7843,7846,7847,7850,7851,7854,7855,7858],{},"§11\u002F§12 배포 직후 사용자(",[32,7799,7800],{},"bubin@malgnsoft.com",", 회사 16)에게서 두 가지 잔존 문제 보고: ",[23,7803,6000],{}," \"파일 선택해도 아무 액션이 없다\" — §11 배포 이전에 가입한 사업자라서 signup auto-create가 안 일어났고, ",[32,7806,6017],{}," 0건 → ",[32,7809,7810],{},"activeContractId=undefined"," → 업로드 분기 무력화. ",[23,7813,6030],{}," \"사업자등록증을 업로드한 상태인데 화면이 '등록해 주세요'(pending)로 나온다\" — §12 배포(17:18 UTC) ",[23,7816,7817],{},"이전인 17:10에 첨부","한 상태라 reviewing 전이가 안 일어나고 ",[32,7820,7821],{},"approval_state=pending"," 그대로. 두 케이스 모두 ",[23,7824,7825],{},"타이밍 race","로 §11\u002F§12 신규 코드가 기존 데이터에는 미적용. 백엔드 두 군데에 ",[23,7828,7829],{},"lazy backfill"," 추가 — ",[32,7832,6464],{},"는 회사 corp\u002Fsole + 계약 0건이면 ",[32,7835,6403],{}," 자동 INSERT, ",[32,7838,6497],{},"는 회사 pending + biz 파일 1건 이상이면 ",[32,7841,6968],{},"으로 자동 UPDATE. 사용자가 새로고침 한 번이면 자동 복구. 회사 16은 즉시 DB UPDATE로 backfill 완료. Workers 배포 #18(",[32,7844,7845],{},"35e2ec85...",") 및 추가 패치 (",[32,7848,7849],{},"456b73c2...","). 프런트 SSR 안전성도 함께 보강 — ",[32,7852,7853],{},"await Promise.all","을 try\u002Fcatch로 감싸고 ",[32,7856,7857],{},"onMounted"," 재시도.",[14,7860,7862],{"id":7861},"_131-두-가지-문제와-원인","13.1 두 가지 문제와 원인",[143,7864,7865,7880],{},[146,7866,7867],{},[149,7868,7869,7871,7874,7877],{},[152,7870,683],{},[152,7872,7873],{},"증상",[152,7875,7876],{},"회사 16 DB 상태",[152,7878,7879],{},"원인",[165,7881,7882,7896],{},[149,7883,7884,7886,7889,7893],{},[170,7885,6000],{},[170,7887,7888],{},"\"파일 선택 후 아무 액션 없음\"",[170,7890,7891,1071],{},[32,7892,6017],{},[170,7894,7895],{},"§11 signup auto-create가 적용된 시점 이전 가입",[149,7897,7898,7900,7903,7908],{},[170,7899,6030],{},[170,7901,7902],{},"\"업로드 후에도 'pending'으로 노출\"",[170,7904,7905,7906],{},"biz 파일 1건 + ",[32,7907,3654],{},[170,7909,7910],{},"첨부 시점이 §12 배포(17:18) 이전(17:10)",[19,7912,7913,7914,7917,7918,1516,7921,7923],{},"(a)는 ",[32,7915,7916],{},"activeContractId"," computed가 첫 계약을 선택하는데 contracts 빈 배열이면 ",[32,7919,7920],{},"undefined",[32,7922,7033],{},"이 \"활성 계약을 찾을 수 없습니다\" 토스트만 떨궈서 사용자에겐 \"아무 일도 안 일어난\" 것처럼 보임. 토스트는 떴지만 짧고 우측 상단이라 놓치기 쉬움.",[19,7925,7926,7927,7929,7930,7932,7933,7936],{},"(b)는 §12의 ",[32,7928,7303],{}," 전이가 ",[32,7931,6510],{}," 시점에만 발동하는 설계라서, 그 코드가 라이브에 올라가기 ",[23,7934,7935],{},"이전","에 이미 첨부한 사용자는 이벤트가 사라진 셈.",[14,7938,7940],{"id":7939},"_132-백엔드-두-군데-lazy-backfill","13.2 백엔드 두 군데 lazy backfill",[1040,7942,7944],{"id":7943},"get-contracts-커밋-6a7a7d2","GET \u002Fcontracts (커밋 6a7a7d2)",[1011,7946,7948],{"className":1658,"code":7947,"language":1660,"meta":1016,"style":1016},"let rows = await select()\nif (rows.length === 0) {\n  const cs = await db.select({ companyType: company.companyType })\n    .from(company).where(eq(company.id, companyId)).limit(1)\n  const ct = cs[0]?.companyType\n  if (ct === 'corp' || ct === 'sole') {\n    await db.insert(contract).values({\n      companyId,\n      title: '최초 이용계약 온라인체결',\n      version: '신규',\n      contractState: 'initial',\n      status: 1,\n    })\n    rows = await select()\n  }\n}\n",[32,7949,7950,7967,7982,7999,8024,8040,8062,8076,8081,8090,8099,8108,8117,8122,8135,8139],{"__ignoreMap":1016},[1020,7951,7952,7955,7958,7960,7962,7965],{"class":1022,"line":1023},[1020,7953,7954],{"class":1667},"let",[1020,7956,7957],{"class":1681}," rows ",[1020,7959,4333],{"class":1667},[1020,7961,3390],{"class":1667},[1020,7963,7964],{"class":1748}," select",[1020,7966,4432],{"class":1681},[1020,7968,7969,7971,7974,7976,7978,7980],{"class":1022,"line":1029},[1020,7970,1742],{"class":1667},[1020,7972,7973],{"class":1681}," (rows.",[1020,7975,3367],{"class":1671},[1020,7977,3370],{"class":1667},[1020,7979,3373],{"class":1671},[1020,7981,4581],{"class":1681},[1020,7983,7984,7986,7988,7990,7992,7994,7996],{"class":1022,"line":1035},[1020,7985,4355],{"class":1667},[1020,7987,7204],{"class":1671},[1020,7989,1675],{"class":1667},[1020,7991,3390],{"class":1667},[1020,7993,4482],{"class":1681},[1020,7995,4485],{"class":1748},[1020,7997,7998],{"class":1681},"({ companyType: company.companyType })\n",[1020,8000,8001,8004,8006,8008,8010,8012,8014,8016,8018,8020,8022],{"class":1022,"line":1739},[1020,8002,8003],{"class":1681},"    .",[1020,8005,4496],{"class":1748},[1020,8007,4499],{"class":1681},[1020,8009,4502],{"class":1748},[1020,8011,1752],{"class":1681},[1020,8013,4507],{"class":1748},[1020,8015,4510],{"class":1681},[1020,8017,4513],{"class":1748},[1020,8019,1752],{"class":1681},[1020,8021,696],{"class":1671},[1020,8023,4520],{"class":1681},[1020,8025,8026,8028,8031,8033,8035,8037],{"class":1022,"line":1764},[1020,8027,4355],{"class":1667},[1020,8029,8030],{"class":1671}," ct",[1020,8032,1675],{"class":1667},[1020,8034,7240],{"class":1681},[1020,8036,4535],{"class":1671},[1020,8038,8039],{"class":1681},"]?.companyType\n",[1020,8041,8042,8044,8047,8049,8051,8053,8056,8058,8060],{"class":1022,"line":1769},[1020,8043,5262],{"class":1667},[1020,8045,8046],{"class":1681}," (ct ",[1020,8048,1685],{"class":1667},[1020,8050,6337],{"class":1688},[1020,8052,6340],{"class":1667},[1020,8054,8055],{"class":1681}," ct ",[1020,8057,1685],{"class":1667},[1020,8059,6348],{"class":1688},[1020,8061,4581],{"class":1681},[1020,8063,8064,8066,8068,8070,8072,8074],{"class":1022,"line":1775},[1020,8065,4611],{"class":1667},[1020,8067,4482],{"class":1681},[1020,8069,6360],{"class":1748},[1020,8071,6363],{"class":1681},[1020,8073,6366],{"class":1748},[1020,8075,6369],{"class":1681},[1020,8077,8078],{"class":1022,"line":1794},[1020,8079,8080],{"class":1681},"      companyId,\n",[1020,8082,8083,8086,8088],{"class":1022,"line":1799},[1020,8084,8085],{"class":1681},"      title: ",[1020,8087,6382],{"class":1688},[1020,8089,6385],{"class":1681},[1020,8091,8092,8095,8097],{"class":1022,"line":3068},[1020,8093,8094],{"class":1681},"      version: ",[1020,8096,6393],{"class":1688},[1020,8098,6385],{"class":1681},[1020,8100,8101,8104,8106],{"class":1022,"line":3083},[1020,8102,8103],{"class":1681},"      contractState: ",[1020,8105,6403],{"class":1688},[1020,8107,6385],{"class":1681},[1020,8109,8110,8113,8115],{"class":1022,"line":4568},[1020,8111,8112],{"class":1681},"      status: ",[1020,8114,696],{"class":1671},[1020,8116,6385],{"class":1681},[1020,8118,8119],{"class":1022,"line":4584},[1020,8120,8121],{"class":1681},"    })\n",[1020,8123,8124,8127,8129,8131,8133],{"class":1022,"line":4602},[1020,8125,8126],{"class":1681},"    rows ",[1020,8128,4333],{"class":1667},[1020,8130,3390],{"class":1667},[1020,8132,7964],{"class":1748},[1020,8134,4432],{"class":1681},[1020,8136,8137],{"class":1022,"line":4608},[1020,8138,4621],{"class":1681},[1020,8140,8141],{"class":1022,"line":4618},[1020,8142,3017],{"class":1681},[19,8144,8145,8147],{},[32,8146,3682],{},"은 trigger 안 함 — 승인 게이트 대상이 아니라서.",[1040,8149,8151],{"id":8150},"get-contractsfiles-커밋-ca657b2","GET \u002Fcontracts\u002Ffiles (커밋 ca657b2)",[1011,8153,8155],{"className":1658,"code":8154,"language":1660,"meta":1016,"style":1016},"const hasBiz = rows.some(r => r.name.startsWith('사업자등록증_'))\nif (hasBiz) {\n  const cs = await db.select({ state: company.approvalState })\n    .from(company).where(eq(company.id, companyId)).limit(1)\n  if (cs[0]?.state === 'pending') {\n    await db.update(company)\n      .set({ approvalState: 'reviewing' })\n      .where(eq(company.id, companyId))\n  }\n}\n",[32,8156,8157,8191,8198,8215,8239,8255,8266,8278,8290,8294],{"__ignoreMap":1016},[1020,8158,8159,8161,8164,8166,8169,8171,8173,8176,8178,8181,8183,8185,8188],{"class":1022,"line":1023},[1020,8160,1668],{"class":1667},[1020,8162,8163],{"class":1671}," hasBiz",[1020,8165,1675],{"class":1667},[1020,8167,8168],{"class":1681}," rows.",[1020,8170,5331],{"class":1748},[1020,8172,1752],{"class":1681},[1020,8174,8175],{"class":1708},"r",[1020,8177,5338],{"class":1667},[1020,8179,8180],{"class":1681}," r.name.",[1020,8182,5354],{"class":1748},[1020,8184,1752],{"class":1681},[1020,8186,8187],{"class":1688},"'사업자등록증_'",[1020,8189,8190],{"class":1681},"))\n",[1020,8192,8193,8195],{"class":1022,"line":1029},[1020,8194,1742],{"class":1667},[1020,8196,8197],{"class":1681}," (hasBiz) {\n",[1020,8199,8200,8202,8204,8206,8208,8210,8212],{"class":1022,"line":1035},[1020,8201,4355],{"class":1667},[1020,8203,7204],{"class":1671},[1020,8205,1675],{"class":1667},[1020,8207,3390],{"class":1667},[1020,8209,4482],{"class":1681},[1020,8211,4485],{"class":1748},[1020,8213,8214],{"class":1681},"({ state: company.approvalState })\n",[1020,8216,8217,8219,8221,8223,8225,8227,8229,8231,8233,8235,8237],{"class":1022,"line":1739},[1020,8218,8003],{"class":1681},[1020,8220,4496],{"class":1748},[1020,8222,4499],{"class":1681},[1020,8224,4502],{"class":1748},[1020,8226,1752],{"class":1681},[1020,8228,4507],{"class":1748},[1020,8230,4510],{"class":1681},[1020,8232,4513],{"class":1748},[1020,8234,1752],{"class":1681},[1020,8236,696],{"class":1671},[1020,8238,4520],{"class":1681},[1020,8240,8241,8243,8245,8247,8249,8251,8253],{"class":1022,"line":1764},[1020,8242,5262],{"class":1667},[1020,8244,7225],{"class":1681},[1020,8246,4535],{"class":1671},[1020,8248,7230],{"class":1681},[1020,8250,1685],{"class":1667},[1020,8252,7235],{"class":1688},[1020,8254,4581],{"class":1681},[1020,8256,8257,8259,8261,8263],{"class":1022,"line":1769},[1020,8258,4611],{"class":1667},[1020,8260,4482],{"class":1681},[1020,8262,7260],{"class":1748},[1020,8264,8265],{"class":1681},"(company)\n",[1020,8267,8268,8270,8272,8274,8276],{"class":1022,"line":1775},[1020,8269,7278],{"class":1681},[1020,8271,7265],{"class":1748},[1020,8273,7268],{"class":1681},[1020,8275,7018],{"class":1688},[1020,8277,7273],{"class":1681},[1020,8279,8280,8282,8284,8286,8288],{"class":1022,"line":1794},[1020,8281,7278],{"class":1681},[1020,8283,4502],{"class":1748},[1020,8285,1752],{"class":1681},[1020,8287,4507],{"class":1748},[1020,8289,7287],{"class":1681},[1020,8291,8292],{"class":1022,"line":1799},[1020,8293,4621],{"class":1681},[1020,8295,8296],{"class":1022,"line":3068},[1020,8297,3017],{"class":1681},[19,8299,8300,2071,8302,2071,8304,8306],{},[32,8301,6968],{},[32,8303,3701],{},[32,8305,3707],{},"인 회사는 손대지 않음 — pending만 보정.",[19,8308,8309,8310,8312,8313,8315],{},"다음 ",[32,8311,249],{}," hydrate(또는 ",[32,8314,5465],{}," 호출)부터 글로벌 띠·페이지 배너 모두 정상.",[14,8317,8319],{"id":8318},"_133-즉시-backfill-회사-16","13.3 즉시 backfill (회사 16)",[1011,8321,8323],{"className":1013,"code":8322,"language":1015,"meta":1016,"style":1016},"INSERT INTO TB_CONTRACT (company_id, title, version, contract_state, status)\nVALUES (16, '최초 이용계약 온라인체결', '신규', 'initial', 1);\n\nUPDATE TB_COMPANY\nSET approval_state='reviewing'\nWHERE id=16;\n",[32,8324,8325,8330,8335,8339,8344,8349],{"__ignoreMap":1016},[1020,8326,8327],{"class":1022,"line":1023},[1020,8328,8329],{},"INSERT INTO TB_CONTRACT (company_id, title, version, contract_state, status)\n",[1020,8331,8332],{"class":1022,"line":1029},[1020,8333,8334],{},"VALUES (16, '최초 이용계약 온라인체결', '신규', 'initial', 1);\n",[1020,8336,8337],{"class":1022,"line":1035},[1020,8338,1730],{"emptyLinePlaceholder":1729},[1020,8340,8341],{"class":1022,"line":1739},[1020,8342,8343],{},"UPDATE TB_COMPANY\n",[1020,8345,8346],{"class":1022,"line":1764},[1020,8347,8348],{},"SET approval_state='reviewing'\n",[1020,8350,8351],{"class":1022,"line":1769},[1020,8352,8353],{},"WHERE id=16;\n",[19,8355,8356],{},"사용자에게 새로고침 안내. 같은 조건의 다른 회사가 있어도 이번 1회로 모두 보정(전체 1건만 해당).",[14,8358,8360],{"id":8359},"_134-프런트-ssr-안전성-보강-커밋-b7e8a21","13.4 프런트 SSR 안전성 보강 (커밋 b7e8a21)",[19,8362,8363,8365,8366,8368],{},[32,8364,6075],{},"의 top-level await가 SSR에서 401·네트워크 실패 시 페이지 전체가 죽었다. try\u002Fcatch로 감싸고 ",[32,8367,7857],{},"에서 한 번 더 시도하도록 변경 — 백엔드 lazy auto-create와 함께 새로고침 한 번으로 정상 복구된다.",[1011,8370,8372],{"className":1658,"code":8371,"language":1660,"meta":1016,"style":1016},"try { await Promise.all([loadContracts(), loadFiles()]) }\ncatch { \u002F* ignore — onMounted에서 재시도 *\u002F }\nonMounted(async () => {\n  if (contracts.value.length === 0 && bizFiles.value.length === 0) {\n    try { await Promise.all([loadContracts(), loadFiles()]) }\n    catch { \u002F* ignore *\u002F }\n  }\n})\n",[32,8373,8374,8403,8415,8431,8457,8482,8494,8498],{"__ignoreMap":1016},[1020,8375,8376,8379,8381,8383,8385,8387,8389,8391,8394,8397,8400],{"class":1022,"line":1023},[1020,8377,8378],{"class":1667},"try",[1020,8380,1705],{"class":1681},[1020,8382,7506],{"class":1667},[1020,8384,3393],{"class":1671},[1020,8386,806],{"class":1681},[1020,8388,3398],{"class":1748},[1020,8390,4268],{"class":1681},[1020,8392,8393],{"class":1748},"loadContracts",[1020,8395,8396],{"class":1681},"(), ",[1020,8398,8399],{"class":1748},"loadFiles",[1020,8401,8402],{"class":1681},"()]) }\n",[1020,8404,8405,8408,8410,8413],{"class":1022,"line":1029},[1020,8406,8407],{"class":1667},"catch",[1020,8409,1705],{"class":1681},[1020,8411,8412],{"class":1735},"\u002F* ignore — onMounted에서 재시도 *\u002F",[1020,8414,3065],{"class":1681},[1020,8416,8417,8419,8421,8424,8427,8429],{"class":1022,"line":1035},[1020,8418,7857],{"class":1748},[1020,8420,1752],{"class":1681},[1020,8422,8423],{"class":1667},"async",[1020,8425,8426],{"class":1681}," () ",[1020,8428,4391],{"class":1667},[1020,8430,4394],{"class":1681},[1020,8432,8433,8435,8438,8440,8442,8444,8446,8449,8451,8453,8455],{"class":1022,"line":1739},[1020,8434,5262],{"class":1667},[1020,8436,8437],{"class":1681}," (contracts.value.",[1020,8439,3367],{"class":1671},[1020,8441,3370],{"class":1667},[1020,8443,3373],{"class":1671},[1020,8445,4409],{"class":1667},[1020,8447,8448],{"class":1681}," bizFiles.value.",[1020,8450,3367],{"class":1671},[1020,8452,3370],{"class":1667},[1020,8454,3373],{"class":1671},[1020,8456,4581],{"class":1681},[1020,8458,8459,8462,8464,8466,8468,8470,8472,8474,8476,8478,8480],{"class":1022,"line":1764},[1020,8460,8461],{"class":1667},"    try",[1020,8463,1705],{"class":1681},[1020,8465,7506],{"class":1667},[1020,8467,3393],{"class":1671},[1020,8469,806],{"class":1681},[1020,8471,3398],{"class":1748},[1020,8473,4268],{"class":1681},[1020,8475,8393],{"class":1748},[1020,8477,8396],{"class":1681},[1020,8479,8399],{"class":1748},[1020,8481,8402],{"class":1681},[1020,8483,8484,8487,8489,8492],{"class":1022,"line":1769},[1020,8485,8486],{"class":1667},"    catch",[1020,8488,1705],{"class":1681},[1020,8490,8491],{"class":1735},"\u002F* ignore *\u002F",[1020,8493,3065],{"class":1681},[1020,8495,8496],{"class":1022,"line":1775},[1020,8497,4621],{"class":1681},[1020,8499,8500],{"class":1022,"line":1794},[1020,8501,5388],{"class":1681},[14,8503,8505],{"id":8504},"_135-산출물","13.5 산출물",[238,8507,8508,8524,8532],{},[241,8509,2646,8510,8513,8514,8516,8517,8520,8521],{},[32,8511,8512],{},"malgn-noti-api: 6a7a7d2, ca657b2"," — 2 파일 수정(",[32,8515,7712],{},"). Workers 배포 #18 Version ",[32,8518,8519],{},"35e2ec85-3e89-4986-b120-d9cf5bbf877b",", 후속 ",[32,8522,8523],{},"456b73c2-c5de-4a99-aece-b4457c0bcd8d",[241,8525,3520,8526,512,8529,8531],{},[32,8527,8528],{},"malgn-noti: b7e8a21",[32,8530,6075],{}," SSR fallback",[241,8533,8534,8535,8537,8538,8541],{},"DB: 회사 16에 ",[32,8536,6017],{}," id=3 backfill + ",[32,8539,8540],{},"approval_state='reviewing'"," UPDATE",[14,8543,8545],{"id":8544},"_136-교훈","13.6 교훈",[19,8547,8548,8549,8552],{},"이번 같은 \"신규 코드 + 기존 데이터\" race는 회원·인증처럼 ",[23,8550,8551],{},"사용자 라이프사이클 이벤트 트리거","가 정책에 묶일 때 흔하다. 두 가지 패턴으로 방어:",[408,8554,8555,8561],{},[241,8556,8557,8560],{},[23,8558,8559],{},"조회 시점 lazy backfill"," — 이번 §13에서 채택. GET 응답을 떠올릴 때 현재 코드가 보장해야 할 상태를 함께 확인·보정. 데이터마이그레이션 미수행 가능.",[241,8562,8563,8566],{},[23,8564,8565],{},"명시적 backfill 마이그레이션"," — DDL이나 SQL 스크립트로 일괄 보정. 정합성은 더 명확하지만 운영 절차 필요.",[19,8568,8569],{},"규칙은 신규 데이터가 적고 정책 trigger가 단순한 경우 1번이 비용 대비 효과 좋음. 향후 같은 패턴(사업자등록증 외 다른 본인확인·서류 흐름)에서도 1번을 default로 두는 것을 권장.",[94,8571],{},[10,8573,8575],{"id":8574},"_14-사업자등록증-파일-행에-심사-상태-배지-반려-시-삭제-pages-7675ce8f","§14. 사업자등록증 파일 행에 심사 상태 배지 + 반려 시 삭제 (Pages 7675ce8f)",[14,8577,103],{"id":8578},"한-줄-13",[19,8580,8581,8582,8585,8586,8589,8590,8593,8594,8596,8597,8599],{},"§12까지의 안내는 패널 상단 카드와 글로벌 띠에 집중되어 있었는데, ",[23,8583,8584],{},"파일 행 자체","에서 상태가 한눈에 안 보였다. 사용자 요청으로 사업자등록증 행 우측에 ",[23,8587,8588],{},"회사 승인 상태 배지","(reviewing\u002Fapproved\u002Frejected) 표시 + ",[23,8591,8592],{},"반려 시에만 삭제 버튼"," 노출. 같은 묶음의 모든 biz 파일이 같은 회사 상태를 공유하므로 모든 행에 동일 배지. 삭제 후에도 회사는 ",[32,8595,3707],{}," 유지(운영자 결정 보존) → 새 파일 첨부 시 백엔드(§12)가 자동 ",[32,8598,6968],{},"으로 전이.",[14,8601,8603],{"id":8602},"_141-화면-변경","14.1 화면 변경",[238,8605,8606,8612,8637,8648],{},[241,8607,8608,8609],{},"파일 행 = ",[32,8610,8611],{},"[아이콘] [이름·메타] [심사 상태 배지] [확인] [(반려 시) 삭제]",[241,8613,8614,8615],{},"배지 색상 매핑:\n",[238,8616,8617,8622,8627,8632],{},[241,8618,8619,8621],{},[32,8620,6968],{}," → info 톤 + 로딩 아이콘 + \"심사 중\"",[241,8623,8624,8626],{},[32,8625,3701],{}," → success 톤 + 체크 + \"승인\"",[241,8628,8629,8631],{},[32,8630,3707],{}," → danger 톤 + X + \"반려\"",[241,8633,8634,8636],{},[32,8635,2288],{}," → 파일 자체가 없으므로 배지 미표시",[241,8638,8639,8640,8643,8644,8647],{},"삭제 버튼은 빨간 outline (",[32,8641,8642],{},".df-remove"," 자체 클래스 — 글로벌 ",[32,8645,8646],{},"btn-outline-danger","가 없어 인라인 정의)",[241,8649,8650,8652,8653,8656,8657,8660],{},[32,8651,7033],{},"이 안정적인 키를 쓰도록 ",[32,8654,8655],{},":key=\"f.id\"","(이전엔 ",[32,8658,8659],{},"name + at"," 조합)",[14,8662,8664],{"id":8663},"_142-삭제-후-상태-정책","14.2 삭제 후 상태 정책",[143,8666,8667,8677],{},[146,8668,8669],{},[149,8670,8671,8674],{},[152,8672,8673],{},"상황",[152,8675,8676],{},"변동",[165,8678,8679,8696,8707],{},[149,8680,8681,8686],{},[170,8682,8683,8685],{},[32,8684,3707],{}," 상태에서 biz 파일 삭제",[170,8687,8688,8689,8691,8692,8695],{},"회사 ",[32,8690,6962],{},"는 ",[23,8693,8694],{},"그대로 rejected"," (사유도 유지)",[149,8697,8698,8701],{},[170,8699,8700],{},"그 뒤 새 파일 첨부",[170,8702,8703,8704,8706],{},"§12 코드가 ",[32,8705,7309],{}," 자동 전이",[149,8708,8709,8716],{},[170,8710,8711,2071,8713,8715],{},[32,8712,6968],{},[32,8714,3701],{}," 중에는",[170,8717,8718],{},"삭제 버튼이 안 보여 사용자 실수 방지",[14,8720,8722],{"id":8721},"_143-산출물","14.3 산출물",[238,8724,8725],{},[241,8726,3520,8727,512,8730,8732,8733],{},[32,8728,8729],{},"malgn-noti: 79e51af",[32,8731,6075],{}," 단일 파일 수정. Pages 배포 alias ",[32,8734,8735],{},"7675ce8f.malgn-noti.pages.dev",[14,8737,8739],{"id":8738},"_144-알려진-한계","14.4 알려진 한계",[238,8741,8742],{},[241,8743,8744,8746],{},[32,8745,6968],{}," 중 잘못 올린 파일을 사용자가 스스로 정정 못함. 정책상 \"심사 중에는 변경 불가\"가 안전하지만, UX적으로 답답할 수 있음. 운영자단 심사 화면이 생기면 같은 곳에서 처리 가능.",[94,8748],{},[10,8750,8752],{"id":8751},"_15-계약서-서명-다이얼로그-휴대폰-본인인증-sub-step-공인인증서-탭-제거-배포-workers-85de422a-pages-38d4e40e573a6200","§15. 계약서 서명 다이얼로그 — 휴대폰 본인인증 sub-step + 공인인증서 탭 제거 (배포 Workers 85de422a \u002F Pages 38d4e40e·573a6200)",[14,8754,103],{"id":8755},"한-줄-14",[19,8757,8758,8760,8761,8764,8765,8768,8769,1480,8772,8775,8776,8778],{},[32,8759,6916],{},"의 STEP 3 \"전자서명\u002F공인인증\" 화면에 ",[23,8762,8763],{},"휴대폰 본인인증 sub-step","을 선행으로 추가. 인증 통과 전엔 서명·정보 테이블 자체가 노출되지 않음. 가입 시 NICE로 검증한 본인 휴대폰(",[32,8766,8767],{},"TB_USER.phone",")으로 SMS OTP를 발송 → 6자리 확인 → 통과 시 success 톤으로 전환되며 서명 캔버스 자동 셋업. 백엔드는 기존 ",[32,8770,8771],{},"POST \u002Fauth\u002Fphone-code\u002F{send,verify}",[32,8773,8774],{},"purpose='contract_sign'","을 새 enum 값으로 추가(기존 인프라 그대로 재활용). 후속 사용자 피드백 두 가지(\"등록 정보 없음으로 노출됨\" \u002F \"공인인증서 삭제\") 반영해 — (a) 다이얼로그 open 시 ",[32,8777,7040],{}," 강제 hydrate로 stale 휴대폰 회복, (b) 공인인증서 탭\u002F영역\u002F관련 CSS·상태 전부 제거해 단일 전자서명 흐름으로 단순화.",[14,8780,8782],{"id":8781},"_151-백엔드-contract_sign-purpose-추가-workers-85de422a","15.1 백엔드 — 'contract_sign' purpose 추가 (Workers 85de422a)",[19,8784,8785,8787,8788,122,8790,2071,8793,8796,8797,8800,8801,8804],{},[32,8786,1536],{}," enum + ",[32,8789,1540],{},[32,8791,8792],{},"sendPhoneCodeB",[32,8794,8795],{},"verifyPhoneCodeB"," zod enum에 ",[32,8798,8799],{},"contract_sign"," 추가. 별도 라우트는 만들지 않고 기존 phone-code 인프라(SHA-256 해시·TTL 10분·5회 시도 제한·재발송 시 직전 코드 무효화·소비 후 재사용 차단) 그대로 재사용. OpenAPI 두 path의 schema enum 동기화. SMS 본문은 ",[32,8802,8803],{},"[맑은 메시징] 계약서 전자서명 인증코드: NNNNNN (10분 유효)"," 형태.",[19,8806,8807,8808,8811],{},"라이브 e2e: 발송 → mockCode 수신 → verify 200 → 소비 후 재시도 401(",[32,8809,8810],{},"인증코드가 만료되었거나 발급된 적이 없습니다",") 모두 정상.",[14,8813,8815],{"id":8814},"_152-다이얼로그-본인인증-sub-step-pages-38d4e40e","15.2 다이얼로그 본인인증 sub-step (Pages 38d4e40e)",[19,8817,8818,503],{},[32,8819,8820],{},"AppContractSignDialog.vue",[238,8822,8823,8826,8833,8839,8846,8856,8861],{},[241,8824,8825],{},"STEP 3 상단에 본인인증 카드(info 톤 → 통과 후 success 톤 전환)",[241,8827,8828,8829,8832],{},"카드 구조: 헤더(strong + p) → 휴대폰 마스킹 표시(",[32,8830,8831],{},"010-****-1111",") + \"인증번호 받기\" → 발송 후 6자리 입력 + \"확인\"",[241,8834,8835,8838],{},[32,8836,8837],{},"phoneVerified === false"," 동안은 서명·정보 테이블·캔버스 모두 미노출",[241,8840,8841,8842,8845],{},"통과 시 ",[32,8843,8844],{},"setupCanvas()"," 직접 호출 (탭 watcher 제거)",[241,8847,8848,8851,8852,8855],{},[32,8849,8850],{},"canComplete"," computed에 ",[32,8853,8854],{},"phoneVerified.value"," 추가 → \"서명 완료\" 버튼 게이팅",[241,8857,8858,8860],{},[32,8859,5742],{},"에 본인인증 가드 추가(방어적)",[241,8862,8863,8866],{},[32,8864,8865],{},"reset()","에 인증 상태 초기화 추가 — 같은 다이얼로그를 닫고 다시 열면 처음부터",[19,8868,8869],{},"부가 정합화:",[238,8871,8872,8892],{},[241,8873,8874,8875,1225,8878,8881,8882,2071,8885,2071,8888,8891],{},"사업자명\u002F대표자 정보가 하드코딩(",[32,8876,8877],{},"(주)맑은소프트",[32,8879,8880],{},"하근호",")이었던 것을 ",[32,8883,8884],{},"auth.tenant.name",[32,8886,8887],{},"bizNo",[32,8889,8890],{},"ceoName","로 동적 바인딩",[241,8893,8894,8895,8898],{},"서명자명 기본값을 ",[32,8896,8897],{},"auth.user.name","으로 자동 채움",[14,8900,8902],{"id":8901},"_153-사용자-피드백-후속-pages-573a6200","15.3 사용자 피드백 후속 (Pages 573a6200)",[408,8904,8905,8927],{},[241,8906,8907,8910,8911,8914,8915,8917,8918],{},[23,8908,8909],{},"\"등록 정보 없음으로 표시됨\""," — 회사 16 user는 phone=",[32,8912,8913],{},"010-1111-1111","이 있는데 다이얼로그가 stale state로 빈 값을 표시. 다이얼로그 open watcher에 ",[32,8916,7040],{}," 강제 호출 추가. 회원정보 수정 직후나 어떤 경로로든 다이얼로그 열릴 때마다 최신 데이터로 hydrate.\n",[238,8919,8920],{},[241,8921,8922,8923,8926],{},"미등록 케이스 안내문구도 강화 — ",[32,8924,8925],{},"회원 정보에 휴대폰 번호가 등록되어 있지 않습니다."," (danger 톤)",[241,8928,8929,512,8932,2071,8935,8938,8939,8942,8943,8946],{},[23,8930,8931],{},"\"공인인증서 삭제\"",[32,8933,8934],{},"signTab",[32,8936,8937],{},"certLoaded"," 상태, ",[32,8940,8941],{},"\u003Cbutton>공인인증서\u003C\u002Fbutton>"," 탭, ",[32,8944,8945],{},".cd-cert-*"," CSS 100여 줄 모두 제거. STEP 3은 본인인증 → 전자서명 단일 흐름.",[14,8948,8950],{"id":8949},"_154-라이브-e2e-production","15.4 라이브 e2e (Production)",[143,8952,8953,8963],{},[146,8954,8955],{},[149,8956,8957,8959,8961],{},[152,8958,683],{},[152,8960,3422],{},[152,8962,689],{},[165,8964,8965,8982,8996,9006],{},[149,8966,8967,8969,8977],{},[170,8968,696],{},[170,8970,8971,1780,8973,8976],{},[32,8972,1470],{},[32,8974,8975],{},"purpose=contract_sign",", mock 모드)",[170,8978,8979,8980],{},"✅ 200 + ",[32,8981,1530],{},[149,8983,8984,8986,8991],{},[170,8985,707],{},[170,8987,8988,8990],{},[32,8989,1473],{}," (올바른 코드)",[170,8992,6829,8993],{},[32,8994,8995],{},"{verified: true}",[149,8997,8998,9000,9003],{},[170,8999,717],{},[170,9001,9002],{},"같은 코드 재시도",[170,9004,9005],{},"✅ 401 — 소비 후 재사용 차단 정상",[149,9007,9008,9010,9013],{},[170,9009,729],{},[170,9011,9012],{},"잘못된 코드",[170,9014,9015,9016],{},"✅ 401 — ",[32,9017,8810],{},[19,9019,9020],{},"E2E 잔존 데이터 cleanup 완료.",[14,9022,9024],{"id":9023},"_155-산출물","15.5 산출물",[238,9026,9027,9040],{},[241,9028,2646,9029,8513,9032,54,9035,7719,9037],{},[32,9030,9031],{},"malgn-noti-api: cd75d0c",[32,9033,9034],{},"routes\u002Fauth.ts",[32,9036,2669],{},[32,9038,9039],{},"85de422a-2ad7-4ce6-929c-8f2b29f03a6e",[241,9041,3520,9042,512,9045,9047,9048,1516,9051],{},[32,9043,9044],{},"malgn-noti: 40979f6, 0054bfc",[32,9046,8820],{}," 단일 파일(235 lines 추가 + 인증서 영역 107 lines 제거). Pages 배포 alias ",[32,9049,9050],{},"38d4e40e",[32,9052,9053],{},"573a6200",[14,9055,9057],{"id":9056},"_156-알려진-한계-후속","15.6 알려진 한계 \u002F 후속",[238,9059,9060,9069,9082],{},[241,9061,9062,9065,9066,9068],{},[23,9063,9064],{},"NICE 자격증명은 §16 참조"," — 현재 NHN_MOCK + NICE_MOCK 둘 다 켜진 mock 모드라서 사용자가 받는 SMS는 실제 발송 안 됨. 토스트에 ",[32,9067,1530],{},"가 노출되어 본인 검증.",[241,9070,9071,9074,9075,9077,9078,9081],{},[23,9072,9073],{},"서명 데이터 보존"," — 현재 캔버스 ink 데이터를 PNG로 저장하지 않음. ",[32,9076,6474],{}," 호출만 백엔드에 보내고 상태만 전이. 법적 효력을 강화하려면 캔버스 이미지를 R2 또는 ",[32,9079,9080],{},"TB_CONTRACT.signed_image_r2_key"," 컬럼에 저장 검토.",[241,9083,9084,9087],{},[23,9085,9086],{},"본인인증 결과 영속화"," — 인증 통과는 다이얼로그 메모리에만 존재. 같은 사용자가 같은 계약 서명을 다시 시작하면 재인증 필요. 정책상 적절 (전자서명법 등본인 동의 강도).",[94,9089],{},[10,9091,9093],{"id":9092},"_16-운영-노트-nice-nhn-notification-hub-자격증명-시도와-보류-라이브-운영-변경","§16. 운영 노트 — NICE \u002F NHN Notification Hub 자격증명 시도와 보류 (라이브 운영 변경)",[14,9095,103],{"id":9096},"한-줄-15",[19,9098,9099,9100,9103,9104,9107,9108,9111,9112,9115,9116,9119,9120,9123],{},"오늘 두 외부 서비스의 production 자격증명 등록을 시도 — 둘 다 ",[23,9101,9102],{},"외부 측 제약","으로 mock 모드 유지 결정. ",[23,9105,9106],{},"NICE","는 자격증명 등록 성공했으나 NICE 콘솔의 ",[23,9109,9110],{},"IP 화이트리스트(에러 1007)"," 미해결로 즉시 mock 복귀. ",[23,9113,9114],{},"NHN Notification Hub","는 사용자가 준 자격이 ",[32,9117,9118],{},"AppKey","만이었는데 공식 문서 확인 결과 ",[23,9121,9122],{},"기존 채널별 SDK와 완전히 다른 인증 모델","(OAuth2 client_credentials → Bearer 토큰)이라 어댑터 재작성 + User Access Key + Secret Access Key 발급 필요. 둘 다 영업·콘솔 작업 대기.",[14,9125,9127],{"id":9126},"_161-nice-본인인증","16.1 NICE 본인인증",[1040,9129,9130],{"id":9130},"시도",[19,9132,9133,9136],{},[32,9134,9135],{},"wrangler secret put","으로 3개 등록:",[238,9138,9139,9148,9157],{},[241,9140,9141,9144,9145],{},[32,9142,9143],{},"NICE_CLIENT_ID"," = ",[32,9146,9147],{},"NIed76e1a1-236a-4cfc-b3b3-4c3586b3dfcf",[241,9149,9150,9144,9153,9156],{},[32,9151,9152],{},"NICE_CLIENT_SECRET",[32,9154,9155],{},"NzY0..."," (전문 보안 사유 생략)",[241,9158,9159,9144,9162],{},[32,9160,9161],{},"NICE_RETURN_URL",[32,9163,9164],{},"https:\u002F\u002Fmalgn-noti-api.malgnsoft.workers.dev\u002Fauth\u002Fnice\u002Fcallback",[19,9166,9167,9170,9171,9173],{},[32,9168,9169],{},"NICE_MOCK"," 삭제 후 ",[32,9172,2274],{}," 호출 → 500 응답.",[1040,9175,9176],{"id":9176},"진단",[19,9178,9179,9182],{},[32,9180,9181],{},"wrangler tail","로 캡처한 에러 로그:",[1011,9184,9189],{"className":9185,"code":9187,"language":9188},[9186],"language-text","(error) [onError] Error: NICE token failed: 1007 허용되지 않은 IP 접근\n","text",[32,9190,9187],{"__ignoreMap":1016},[19,9192,9193,9194,9197,9198,9201],{},"NICE 콘솔의 API 보안 정책에 ",[23,9195,9196],{},"호출 출발지 IP 화이트리스트","가 활성화된 상태. Cloudflare Workers는 outbound IP가 동적이라 단일 IP 등록 불가. ",[32,9199,9200],{},"doc\u002FNICE_AUTH.md §9","에서 사전 예측한 한계 그대로.",[1040,9203,9204],{"id":9204},"결정",[238,9206,9207,9213],{},[241,9208,9209,9212],{},[32,9210,9211],{},"NICE_MOCK=1"," 다시 등록 → 가입 흐름 mock 모드 복귀(정상 동작 확인)",[241,9214,9215,1225,9217,1225,9219,9221,9222,9225,9226,9228],{},[32,9216,9143],{},[32,9218,9152],{},[32,9220,9161],{}," 3 secret은 ",[23,9223,9224],{},"유지"," — IP 정책 해결 시 ",[32,9227,2726],{}," 한 번이면 real 전환",[1040,9230,9232],{"id":9231},"해결-옵션-사용자-결정-대기","해결 옵션 (사용자 결정 대기)",[143,9234,9235,9247],{},[146,9236,9237],{},[149,9238,9239,9242,9245],{},[152,9240,9241],{},"옵션",[152,9243,9244],{},"작업",[152,9246,4674],{},[165,9248,9249,9260,9271],{},[149,9250,9251,9254,9257],{},[170,9252,9253],{},"A. NICE 콘솔에서 Cloudflare egress IP 등록",[170,9255,9256],{},"NICE 영업담당에게 IP 목록 송부 후 콘솔 반영",[170,9258,9259],{},"통상 거절될 가능성",[149,9261,9262,9265,9268],{},[170,9263,9264],{},"B. NICE 콘솔에서 IP 검사 OFF",[170,9266,9267],{},"콘솔 → API 설정 토글 해제",[170,9269,9270],{},"가장 단순, 보안 등급은 다소 낮아짐",[149,9272,9273,9276,9279],{},[170,9274,9275],{},"C. 고정 IP 프록시 EC2",[170,9277,9278],{},"AWS EC2 nano, Workers → EC2 → NICE",[170,9280,9281],{},"가장 안정, 월 비용 발생",[19,9283,9284,9285,9288],{},"사용자 의사로 IP 정책은 일단 ",[23,9286,9287],{},"보류",", 자격증명만 보관 상태.",[1040,9290,9292],{"id":9291},"보안-메모","보안 메모",[19,9294,9295,9298,9299,9302],{},[32,9296,9297],{},"CLIENT_SECRET","이 채팅 평문에 노출됐다. IP 정책 해결 시점에 NICE 콘솔에서 한 번 회전 권장. 회전 후 ",[32,9300,9301],{},"wrangler secret put NICE_CLIENT_SECRET"," 재등록.",[14,9304,9306],{"id":9305},"_162-nhn-notification-hub","16.2 NHN Notification Hub",[1040,9308,9310],{"id":9309},"사용자-제공","사용자 제공",[238,9312,9313,9319,9325],{},[241,9314,9315,9316],{},"AppKey: ",[32,9317,9318],{},"JhgDNGyD9dyYQqH5",[241,9320,9321,9322],{},"BaseURL: ",[32,9323,9324],{},"https:\u002F\u002Fnotification-hub.api.nhncloudservice.com",[241,9326,9327,9328],{},"Secret Key: ",[23,9329,9330],{},"제공되지 않음",[1040,9332,9334],{"id":9333},"_1차-진단-기존-채널별-sdk-가정","1차 진단 — 기존 채널별 SDK 가정",[19,9336,9337,9338,9341,9342,9345,9346,9349],{},"현재 어댑터(",[32,9339,9340],{},"src\u002Fadapters\u002Fnhn\u002F{sms,email,push,kakao}.ts",")는 채널별 분리 API(",[32,9343,9344],{},"https:\u002F\u002Fapi-sms.cloud.toast.com"," 등)에 대해 작성됨. 인증은 ",[32,9347,9348],{},"X-Secret-Key"," 헤더. 사용자에게 SecretKey 요청.",[1040,9351,9353],{"id":9352},"_2차-진단-공식-문서-확인","2차 진단 — 공식 문서 확인",[19,9355,9356,9362],{},[255,9357,9361],{"href":9358,"rel":9359},"https:\u002F\u002Fdocs.nhncloud.com\u002Fko\u002FNotification\u002FNotification%20Hub\u002Fko\u002Fapi-guide-v1x0\u002Fcommon-info\u002F",[9360],"nofollow","NHN Cloud Notification Hub 공통 정보"," 확인 결과:",[19,9364,9365],{},[23,9366,9367],{},"Notification Hub는 기존 NHN 채널별 API와 완전히 다른 신규 통합 서비스.",[143,9369,9370,9384],{},[146,9371,9372],{},[149,9373,9374,9376,9379],{},[152,9375,929],{},[152,9377,9378],{},"기존 채널별 NHN",[152,9380,9381],{},[23,9382,9383],{},"Notification Hub",[165,9385,9386,9401,9415,9429],{},[149,9387,9388,9391,9396],{},[170,9389,9390],{},"인증 헤더",[170,9392,9393],{},[32,9394,9395],{},"X-Secret-Key: \u003C키>",[170,9397,9398],{},[32,9399,9400],{},"Authorization: Bearer \u003C토큰>",[149,9402,9403,9406,9409],{},[170,9404,9405],{},"자격 종류",[170,9407,9408],{},"AppKey + SecretKey (정적)",[170,9410,9411,9414],{},[23,9412,9413],{},"User Access Key ID + Secret Access Key"," (OAuth2)",[149,9416,9417,9420,9423],{},[170,9418,9419],{},"토큰 발급",[170,9421,9422],{},"불필요",[170,9424,9425,9428],{},[32,9426,9427],{},"POST https:\u002F\u002Foauth.api.nhncloudservice.com\u002Foauth2\u002Ftoken\u002Fcreate"," (Bearer, TTL 24h)",[149,9430,9431,9434,9437],{},[170,9432,9433],{},"AppKey 역할",[170,9435,9436],{},"경로 + 인증",[170,9438,9439,9440,9443],{},"JWT 토큰 발급 시 scope(",[32,9441,9442],{},"scope=appKey:\u003CAppKey>",")에만 사용",[19,9445,9446],{},"토큰 발급 cURL:",[1011,9448,9450],{"className":4745,"code":9449,"language":4747,"meta":1016,"style":1016},"curl -X POST 'https:\u002F\u002Foauth.api.nhncloudservice.com\u002Foauth2\u002Ftoken\u002Fcreate' \\\n  -H 'Content-Type: application\u002Fx-www-form-urlencoded' \\\n  -u 'UserAccessKeyID:SecretAccessKey' \\\n  -d 'grant_type=client_credentials' \\\n  -d 'scope=appKey:\u003CAppKey>'\n",[32,9451,9452,9469,9479,9489,9499],{"__ignoreMap":1016},[1020,9453,9454,9457,9460,9463,9466],{"class":1022,"line":1023},[1020,9455,9456],{"class":1748},"curl",[1020,9458,9459],{"class":1671}," -X",[1020,9461,9462],{"class":1688}," POST",[1020,9464,9465],{"class":1688}," 'https:\u002F\u002Foauth.api.nhncloudservice.com\u002Foauth2\u002Ftoken\u002Fcreate'",[1020,9467,9468],{"class":1671}," \\\n",[1020,9470,9471,9474,9477],{"class":1022,"line":1029},[1020,9472,9473],{"class":1671},"  -H",[1020,9475,9476],{"class":1688}," 'Content-Type: application\u002Fx-www-form-urlencoded'",[1020,9478,9468],{"class":1671},[1020,9480,9481,9484,9487],{"class":1022,"line":1035},[1020,9482,9483],{"class":1671},"  -u",[1020,9485,9486],{"class":1688}," 'UserAccessKeyID:SecretAccessKey'",[1020,9488,9468],{"class":1671},[1020,9490,9491,9494,9497],{"class":1022,"line":1739},[1020,9492,9493],{"class":1671},"  -d",[1020,9495,9496],{"class":1688}," 'grant_type=client_credentials'",[1020,9498,9468],{"class":1671},[1020,9500,9501,9503],{"class":1022,"line":1764},[1020,9502,9493],{"class":1671},[1020,9504,9505],{"class":1688}," 'scope=appKey:\u003CAppKey>'\n",[1040,9507,9204],{"id":9508},"결정-1",[19,9510,9511],{},"User Access Key ID + Secret Access Key를 받으면 다음을 한 번에 진행:",[408,9513,9514,9520,9535,9540],{},[241,9515,9516,9519],{},[23,9517,9518],{},"NHN 어댑터 재작성"," — 채널별 SDK → Notification Hub 통합 API. 경로 구조와 페이로드 모두 변경. OAuth 토큰 발급 + KV 캐싱(24h) 헬퍼 추가.",[241,9521,9522,1225,9525,1225,9528,1225,9531,9534],{},[32,9523,9524],{},"wrangler secret put NHN_OAUTH_USER_KEY",[32,9526,9527],{},"NHN_OAUTH_SECRET_KEY",[32,9529,9530],{},"NHN_APP_KEY",[32,9532,9533],{},"NHN_BASE_URL"," 4개 등록.",[241,9536,9537,806],{},[32,9538,9539],{},"wrangler secret delete NHN_MOCK",[241,9541,9542],{},"e2e — SMS 1건 + Email 1건 실 발송.",[19,9544,9545,9546,9548,9549,9551],{},"지금은 자격 미수령 + 어댑터 재작성 미진행 → 사용자단·관리자단의 모든 발송 호출은 ",[32,9547,1526],{}," 그대로 mock 모드 유지(가입 OTP 토스트의 ",[32,9550,1530],{},"로 검증 정상).",[1040,9553,9555],{"id":9554},"코드-변경-없음","코드 변경 없음",[19,9557,9558,9559,9562,9563,9565],{},"이 §16은 운영 시도 + 외부 제약 확인 + 보류 결정의 기록. ",[23,9560,9561],{},"코드\u002F배포 변경은 없음","(자격 secret put\u002Fdelete + ",[32,9564,9169],{}," 재등록만). 어댑터 재작성은 키 수령 시점에 §17 이후로 별도.",[14,9567,9569],{"id":9568},"_163-산출물","16.3 산출물",[238,9571,9572,9575,9602],{},[241,9573,9574],{},"코드: 없음",[241,9576,9577,9578],{},"secret 변경(production Workers):\n",[238,9579,9580,9592],{},[241,9581,9582,1225,9585,1225,9588,9591],{},[32,9583,9584],{},"+NICE_CLIENT_ID",[32,9586,9587],{},"+NICE_CLIENT_SECRET",[32,9589,9590],{},"+NICE_RETURN_URL"," — 등록 후 유지",[241,9593,9594,9597,9598,9601],{},[32,9595,9596],{},"-NICE_MOCK"," 일시 삭제 → ",[32,9599,9600],{},"+NICE_MOCK=1"," 복원",[241,9603,9604,9605],{},"외부 미해결:\n",[238,9606,9607,9610],{},[241,9608,9609],{},"NICE: 1007 IP 화이트리스트 (사용자 콘솔 작업)",[241,9611,9612],{},"NHN: User Access Key 발급 (사용자 콘솔 작업)",[14,9614,9616],{"id":9615},"_164-다음-단계","16.4 다음 단계",[143,9618,9619,9630],{},[146,9620,9621],{},[149,9622,9623,9625,9628],{},[152,9624,929],{},[152,9626,9627],{},"트리거",[152,9629,9244],{},[165,9631,9632,9645,9656],{},[149,9633,9634,9637,9640],{},[170,9635,9636],{},"NICE real 전환",[170,9638,9639],{},"사용자가 IP 정책 해결 (옵션 B 권장)",[170,9641,9642,9644],{},[32,9643,2726],{}," + e2e 1건",[149,9646,9647,9650,9653],{},[170,9648,9649],{},"NHN real 전환",[170,9651,9652],{},"사용자가 User Access Key 발급",[170,9654,9655],{},"어댑터 재작성 + secret 등록 + e2e SMS·Email",[149,9657,9658,9661,9664],{},[170,9659,9660],{},"사용자 안내 자동화",[170,9662,9663],{},"위 두 trigger 발생 시",[170,9665,9666],{},"메일 발송 + SMS 통지 인프라가 정확히 위 두 secret에 의존하므로 동시에 enable",[9668,9669,9670],"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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":1016,"searchDepth":1035,"depth":1035,"links":9672},[9673,9674,9675,9676,9677,9678,9679,9680,9681,9682,9684,9686,9687,9688,9689,9690,9691,9692,9695,9696,9697,9698,9699,9700,9701,9702,9703,9704,9706,9707,9708,9709,9710,9711,9712,9713,9715,9717,9719,9720,9721,9723,9724,9725,9726,9727,9728,9729,9730,9731,9732,9733,9734,9735,9740,9745,9746,9747,9748,9749,9751,9752,9753,9754,9755,9756,9758,9760,9762,9763,9764,9765,9766,9767,9768,9769,9770,9771,9772,9774,9775,9776,9777,9779,9780,9782,9783,9784,9785,9786,9788,9793,9800,9801,9802,9803,9804,9805,9809,9810,9811,9812,9813,9814,9815,9816,9817,9818,9819,9820,9821,9822,9823,9824,9825,9826,9833,9840,9841],{"id":16,"depth":1029,"text":17},{"id":102,"depth":1029,"text":103},{"id":133,"depth":1029,"text":134},{"id":140,"depth":1029,"text":141},{"id":232,"depth":1029,"text":233},{"id":262,"depth":1029,"text":263},{"id":375,"depth":1029,"text":376},{"id":399,"depth":1029,"text":400},{"id":458,"depth":1029,"text":103},{"id":494,"depth":1029,"text":9683},"2.1 백엔드 — POST \u002Fauth\u002Flogin-by-email",{"id":592,"depth":1029,"text":9685},"2.2 프런트 — login\u002Findex.vue 개편",{"id":673,"depth":1029,"text":674},{"id":767,"depth":1029,"text":768},{"id":812,"depth":1029,"text":813},{"id":840,"depth":1029,"text":841},{"id":873,"depth":1029,"text":103},{"id":919,"depth":1029,"text":920},{"id":1002,"depth":1029,"text":1003,"children":9693},[9694],{"id":1042,"depth":1035,"text":1043},{"id":1102,"depth":1029,"text":1103},{"id":1177,"depth":1029,"text":1178},{"id":1251,"depth":1029,"text":1252},{"id":1319,"depth":1029,"text":1320},{"id":1358,"depth":1029,"text":1359},{"id":1419,"depth":1029,"text":1420},{"id":1446,"depth":1029,"text":103},{"id":1464,"depth":1029,"text":1465},{"id":1572,"depth":1029,"text":1573},{"id":1635,"depth":1029,"text":9705},"4.3 useApi.ts 401 처리 분리 — \u002Fauth\u002F*는 호출자가 처리",{"id":1805,"depth":1029,"text":1806},{"id":1816,"depth":1029,"text":1817},{"id":1865,"depth":1029,"text":1866},{"id":1900,"depth":1029,"text":1901},{"id":1917,"depth":1029,"text":103},{"id":1939,"depth":1029,"text":1940},{"id":2021,"depth":1029,"text":2022},{"id":2156,"depth":1029,"text":9714},"5.3 NICE 어댑터 — src\u002Fadapters\u002Fnice\u002Fauth.ts",{"id":2249,"depth":1029,"text":9716},"5.4 라우트 — src\u002Froutes\u002Fnice.ts",{"id":2325,"depth":1029,"text":9718},"5.5 \u002Fauth\u002Fsignup 확장",{"id":2392,"depth":1029,"text":2393},{"id":2473,"depth":1029,"text":2474},{"id":2585,"depth":1029,"text":9722},"5.8 정본 문서 — doc\u002FNICE_AUTH.md",{"id":2640,"depth":1029,"text":2641},{"id":2711,"depth":1029,"text":2712},{"id":2789,"depth":1029,"text":103},{"id":2827,"depth":1029,"text":2828},{"id":2987,"depth":1029,"text":2988},{"id":3088,"depth":1029,"text":3089},{"id":3410,"depth":1029,"text":3411},{"id":3500,"depth":1029,"text":3501},{"id":3535,"depth":1029,"text":3536},{"id":3586,"depth":1029,"text":103},{"id":3620,"depth":1029,"text":3621},{"id":3712,"depth":1029,"text":3713},{"id":3758,"depth":1029,"text":3759,"children":9736},[9737,9738,9739],{"id":3762,"depth":1035,"text":3763},{"id":3796,"depth":1035,"text":3797},{"id":3817,"depth":1035,"text":3818},{"id":3856,"depth":1029,"text":3857,"children":9741},[9742,9743,9744],{"id":3860,"depth":1035,"text":2460},{"id":3885,"depth":1035,"text":2678},{"id":3911,"depth":1035,"text":2808},{"id":3951,"depth":1029,"text":3952},{"id":4075,"depth":1029,"text":4076},{"id":4122,"depth":1029,"text":4123},{"id":4201,"depth":1029,"text":103},{"id":4235,"depth":1029,"text":9750},"8.1 미들웨어 — src\u002Fmiddleware\u002Fapproval.ts",{"id":4656,"depth":1029,"text":4657},{"id":4868,"depth":1029,"text":4869},{"id":4963,"depth":1029,"text":4964},{"id":4980,"depth":1029,"text":4981},{"id":5028,"depth":1029,"text":103},{"id":5064,"depth":1029,"text":9757},"9.1 글로벌 띠 — AppApprovalBanner.vue",{"id":5175,"depth":1029,"text":9759},"9.2 글로벌 라우트 가드 — middleware\u002Fapproval.global.ts",{"id":5469,"depth":1029,"text":9761},"9.3 \u002Fhome 페이지 미승인 분기",{"id":5516,"depth":1029,"text":5517},{"id":5569,"depth":1029,"text":5570},{"id":5593,"depth":1029,"text":5594},{"id":5652,"depth":1029,"text":103},{"id":5683,"depth":1029,"text":5684},{"id":5770,"depth":1029,"text":5771},{"id":5834,"depth":1029,"text":5835},{"id":5927,"depth":1029,"text":5928},{"id":5941,"depth":1029,"text":5942},{"id":5991,"depth":1029,"text":103},{"id":6091,"depth":1029,"text":9773},"11.1 결정 — TB_CONTRACT_FILE.kind 컬럼 없음 → name 접두사 사용",{"id":6185,"depth":1029,"text":6186},{"id":6225,"depth":1029,"text":6226},{"id":6314,"depth":1029,"text":6315},{"id":6434,"depth":1029,"text":9778},"11.5 \u002Fcontracts 라우트 — 5 엔드포인트",{"id":6564,"depth":1029,"text":6565},{"id":6611,"depth":1029,"text":9781},"11.7 프런트 — AppContractPanel.vue 실 API 연동",{"id":6761,"depth":1029,"text":6762},{"id":6850,"depth":1029,"text":6851},{"id":6899,"depth":1029,"text":6900},{"id":6956,"depth":1029,"text":103},{"id":7051,"depth":1029,"text":9787},"12.1 정책 — approval_state 4단계로 확장",{"id":7170,"depth":1029,"text":7171,"children":9789},[9790,9792],{"id":7174,"depth":1035,"text":9791},"POST \u002Fcontracts\u002Ffiles (kind=biz만 발동)",{"id":7331,"depth":1035,"text":7332},{"id":7389,"depth":1029,"text":7390,"children":9794},[9795,9796,9797,9798,9799],{"id":7393,"depth":1035,"text":7394},{"id":7407,"depth":1035,"text":7408},{"id":7444,"depth":1035,"text":7445},{"id":7456,"depth":1035,"text":7457},{"id":7495,"depth":1035,"text":7496},{"id":7618,"depth":1029,"text":7619},{"id":7700,"depth":1029,"text":7701},{"id":7741,"depth":1029,"text":7742},{"id":7794,"depth":1029,"text":103},{"id":7861,"depth":1029,"text":7862},{"id":7939,"depth":1029,"text":7940,"children":9806},[9807,9808],{"id":7943,"depth":1035,"text":7944},{"id":8150,"depth":1035,"text":8151},{"id":8318,"depth":1029,"text":8319},{"id":8359,"depth":1029,"text":8360},{"id":8504,"depth":1029,"text":8505},{"id":8544,"depth":1029,"text":8545},{"id":8578,"depth":1029,"text":103},{"id":8602,"depth":1029,"text":8603},{"id":8663,"depth":1029,"text":8664},{"id":8721,"depth":1029,"text":8722},{"id":8738,"depth":1029,"text":8739},{"id":8755,"depth":1029,"text":103},{"id":8781,"depth":1029,"text":8782},{"id":8814,"depth":1029,"text":8815},{"id":8901,"depth":1029,"text":8902},{"id":8949,"depth":1029,"text":8950},{"id":9023,"depth":1029,"text":9024},{"id":9056,"depth":1029,"text":9057},{"id":9096,"depth":1029,"text":103},{"id":9126,"depth":1029,"text":9127,"children":9827},[9828,9829,9830,9831,9832],{"id":9130,"depth":1035,"text":9130},{"id":9176,"depth":1035,"text":9176},{"id":9204,"depth":1035,"text":9204},{"id":9231,"depth":1035,"text":9232},{"id":9291,"depth":1035,"text":9292},{"id":9305,"depth":1029,"text":9306,"children":9834},[9835,9836,9837,9838,9839],{"id":9309,"depth":1035,"text":9310},{"id":9333,"depth":1035,"text":9334},{"id":9352,"depth":1035,"text":9353},{"id":9508,"depth":1035,"text":9204},{"id":9554,"depth":1035,"text":9555},{"id":9568,"depth":1029,"text":9569},{"id":9615,"depth":1029,"text":9616},"md",{},"\u002Fhistory\u002Fhistory.20260602",{"title":5,"description":1016},"history\u002Fhistory.20260602","qSA_cFf7cbjHG83YXzNS-tN2bwvJoG2cLVnlVx8NWMQ",1780639567004]