[{"data":1,"prerenderedAt":4535},["ShallowReactive",2],{"doc:\u002Fhistory\u002Fhistory.20260526":3},{"id":4,"title":5,"body":6,"description":563,"extension":4528,"meta":4529,"navigation":4530,"path":4531,"seo":4532,"stem":4533,"__hash__":4534},"docs\u002Fhistory\u002Fhistory.20260526.md","2026-05-26 — malgn-noti-api 데이터 모델·초기 DDL·Hyperdrive 연결·첫 프로덕션 배포 + 운영 컨벤션 명문화 + malgn-noti 배포 #53 + Aurora DDL 적용 + 기본 CRUD API 골격 + \u002Fdoc + 두 번째 프로덕션 배포",{"type":7,"value":8,"toc":4404},"minimark",[9,13,18,112,120,298,305,339,346,358,465,472,534,542,649,653,697,701,746,750,758,798,802,830,834,854,860,866,881,947,957,961,976,1060,1064,1071,1082,1170,1174,1182,1278,1281,1338,1342,1383,1387,1414,1422,1429,1436,1446,1558,1611,1621,1624,1651,1661,1672,1676,1708,1721,1728,1734,1765,1771,1788,1794,1799,1861,1866,1907,1914,1938,1945,2046,2049,2096,2100,2103,2131,2134,2160,2171,2175,2189,2196,2312,2316,2339,2343,2349,2357,2361,2368,2375,2381,2385,2401,2408,2510,2513,2517,2527,2534,2537,2543,2546,2609,2613,2803,2807,2836,2840,2843,2872,2876,2912,2925,2928,2934,2940,2986,2992,2995,2998,3042,3045,3055,3058,3071,3074,3094,3097,3105,3111,3114,3117,3139,3146,3183,3186,3204,3207,3210,3221,3225,3228,3232,3249,3253,3272,3276,3393,3399,3403,3410,3414,3425,3432,3436,3473,3477,3499,3503,3528,3540,3544,3554,3579,3583,3586,3646,3650,3656,3660,3673,3680,3798,3802,3813,3817,3823,3827,3844,3848,3864,3887,3893,3900,3903,3907,3979,3983,4016,4020,4053,4057,4069,4073,4082,4086,4125,4129,4191,4201,4205,4218,4242,4245,4301,4305,4312,4316,4400],[10,11,5],"h1",{"id":12},"_2026-05-26-malgn-noti-api-데이터-모델초기-ddlhyperdrive-연결첫-프로덕션-배포-운영-컨벤션-명문화-malgn-noti-배포-53-aurora-ddl-적용-기본-crud-api-골격-doc-두-번째-프로덕션-배포",[14,15,17],"h2",{"id":16},"한-줄-요약","한 줄 요약",[19,20,21,25,26,30,31,34,35,38,39,42,43,46,47,50,51,54,55,58,59,62,63,66,67,70,71,77,78,81,82,85,86,89,90,96,97,99,100,103,104,107,108,111],"p",{},[22,23,24],"code",{},"malgn-noti-api","의 ",[27,28,29],"strong",{},"데이터 모델링부터 첫 프로덕션 배포까지"," 한 흐름으로 진행 — 사용자단 화면·소스·MD를 읽고 49개 테이블 데이터 모델 작성(",[27,32,33],{},"TB_"," 접두어, ",[22,36,37],{},"company_id"," FK, ",[22,40,41],{},"status INT"," 1\u002F0\u002F-1 + ",[22,44,45],{},"*_state VARCHAR"," 분리, ",[22,48,49],{},"*_yn CHAR(1)"," Y\u002FN, ",[22,52,53],{},"loginid","\u002F",[22,56,57],{},"email"," 분리), 시각 ERD를 Mermaid 9종으로 작성, 발송량 시나리오 분석 후 ",[27,60,61],{},"월 RANGE 파티셔닝 + Hot\u002FWarm\u002FCold + R2 오프로드"," 확장성 전략을 정본 §13 + 별도 ",[22,64,65],{},"SCALABILITY.md","로 정리, 49 테이블 초기 마이그레이션 SQL(파티션 5종 + ",[22,68,69],{},"raw_payload_r2_key"," 포함)을 작성, ",[27,72,73,74],{},"Hyperdrive(MySQL) 바인딩 ",[22,75,76],{},"a2ba4efe7421464da1d5ff5e620b33a3"," 연결 + ",[22,79,80],{},"drizzle-orm\u002Fmysql2"," 셋업 + ",[22,83,84],{},"\u002Fhealth\u002Fdb"," 헬스 체크 + ",[22,87,88],{},"wrangler dev --remote"," 로 로컬에서도 실제 Aurora MySQL 8.0.42 응답 확인, ",[27,91,92,95],{},[22,93,94],{},"https:\u002F\u002Fmalgn-noti-api.malgnsoft.workers.dev"," 프로덕션 첫 배포"," 완료 (모든 엔드포인트 200, ",[22,98,84],{}," mysql_version 8.0.42). 이어 ",[22,101,102],{},"malgn-noti-api\u002FCLAUDE.md §8.1","에 ",[27,105,106],{},"배포·Git·작업 이력 운영 컨벤션을 명문화","하여, 프론트와 동일한 디스플린(typecheck → 배포 → 검증 → 커밋·푸시·history)을 백엔드에서도 강제하도록 정리. 작업 이력은 ",[22,109,110],{},"malgn-noti\u002Fdoc\u002Fhistory\u002F","가 3 레포 공통 정본임을 §8.1에 못박음.",[14,113,115,116,119],{"id":114},"_1-데이터-모델-malgn-noti-apidocdata-modelmd","1. 데이터 모델 (",[22,117,118],{},"malgn-noti-api\u002Fdoc\u002FDATA-MODEL.md",")",[121,122,123,143,149,280,295],"ul",{},[124,125,126,127,130,131,134,135,138,139,142],"li",{},"입력: ",[22,128,129],{},"malgn-noti","의 화면 73개·",[22,132,133],{},"app\u002Ftypes\u002F*","·목업 데이터, ",[22,136,137],{},"malgn-noti-api\u002FCLAUDE.md §5 1차 모델",", ",[22,140,141],{},"doc\u002FDESIGN.md §14",".",[124,144,145,148],{},[27,146,147],{},"49개 테이블"," \u002F 9개 도메인. Aurora MySQL 8.0 \u002F Drizzle 대상.",[124,150,151,152],{},"공통 규칙:\n",[121,153,154,166,179,189,209,232,257,267],{},[124,155,156,161,162,165],{},[27,157,158,160],{},[22,159,33],{}," 접두어 + 대문자 스네이크"," (",[22,163,164],{},"TB_DISPATCH_REQUEST",").",[124,167,168,169,172,173,175,176,165],{},"컬럼 ",[22,170,171],{},"snake_case",", 고객사 FK는 ",[22,174,37],{},". 멀티 테넌트 격리(",[22,177,178],{},"§1.2",[124,180,181,182,185,186,142],{},"시간 ",[22,183,184],{},"DATETIME"," UTC. PK ",[22,187,188],{},"BIGINT UNSIGNED AUTO_INCREMENT",[124,190,191,192,195,196,54,199,202,203,161,206,165],{},"불리언 ",[22,193,194],{},"CHAR(1)"," ",[22,197,198],{},"'Y'",[22,200,201],{},"'N'",", 컬럼명 ",[22,204,205],{},"*_yn",[22,207,208],{},"§1.11",[124,210,211,216,217,220,221,224,225,228,229,165],{},[27,212,213],{},[22,214,215],{},"status INT NOT NULL DEFAULT 1"," — ",[22,218,219],{},"1","=정상\u002F",[22,222,223],{},"0","=중지\u002F",[22,226,227],{},"-1","=삭제 (",[22,230,231],{},"§1.12",[124,233,234,235,240,241,54,244,54,247,54,250,54,253,256],{},"다단계 업무 상태는 ",[27,236,237],{},[22,238,239],{},"*_state VARCHAR(20)"," 으로 분리 — ",[22,242,243],{},"dispatch_state",[22,245,246],{},"review_state",[22,248,249],{},"approval_state",[22,251,252],{},"pay_state",[22,254,255],{},"answer_state"," 등 16개.",[124,258,259,262,263,266],{},[22,260,261],{},"enum"," 회피 → ",[22,264,265],{},"VARCHAR"," + Zod 검증.",[124,268,269,270,138,273,138,276,279],{},"JSON 컬럼(",[22,271,272],{},"spec",[22,274,275],{},"message_spec",[22,277,278],{},"nodes",")으로 채널 형상 다양성 흡수.",[124,281,282,216,285,287,288,291,292,294],{},[27,283,284],{},"TB_USER",[22,286,53],{}," (로그인 ID, ",[22,289,290],{},"UNIQUE(company_id, loginid)",") + ",[22,293,57],{}," (알림·영수증 수신) 분리.",[124,296,297],{},"도메인별 §3~§10 — 계정\u002F인증, 크레딧\u002F결제, 발신정보, 주소록, 템플릿, 발송·Flow·캠페인, 이력\u002FExport, 문의\u002F시스템.",[14,299,301,302,119],{"id":300},"_2-erd-malgn-noti-apidocerdmd","2. ERD (",[22,303,304],{},"malgn-noti-api\u002Fdoc\u002FERD.md",[121,306,307,317,324],{},[124,308,309,310,195,313,316],{},"Mermaid ",[22,311,312],{},"erDiagram",[27,314,315],{},"9종"," — 전체 관계 개관 1 + 도메인 8 (§2~§9).",[124,318,319,320,323],{},"모든 컬럼에 코멘트 4번째 항목 추가 — 박스 내부에 ",[22,321,322],{},"bigint id PK \"고객사 식별자\""," 형태로 렌더링.",[124,325,326,327,330,331,334,335,338],{},"카디널리티 ",[22,328,329],{},"||--o{","(1:N)·",[22,332,333],{},"||--||","(1:1)·",[22,336,337],{},"||--o|","(1:0\u002F1)·M:N(junction)·자기참조(트리·중첩답글)·복합 PK 모두 표현.",[14,340,342,343,119],{"id":341},"_3-확장성파티셔닝-전략-malgn-noti-apidocscalabilitymd","3. 확장성·파티셔닝 전략 (",[22,344,345],{},"malgn-noti-api\u002Fdoc\u002FSCALABILITY.md",[19,347,348,349,357],{},"볼륨 가정(1년차 100만",[350,351,352,353,356],"del",{},"1천만\u002F월 → 5년차 1억+\u002F월)에서 13억 행 Aurora 누적 시나리오를 분석하고, ",[27,354,355],{},"DDL 첫 작성 시점부터"," 적용해야 사후 마이그레이션 비용을 회피할 수 있는 결정 사항을 §1","§7로 정리.",[121,359,360,392,405,418,434,440,453],{},[124,361,362,216,365,368,369,368,372,375,376,379,380,383,384,387,388,391],{},[27,363,364],{},"§1 월 RANGE 파티셔닝",[22,366,367],{},"TB_DISPATCH_REQUEST\u002FITEM\u002FEVENT"," + ",[22,370,371],{},"TB_CREDIT_LEDGER",[22,373,374],{},"TB_AUDIT_LOG"," 5개. PK 복합 ",[22,377,378],{},"(id, created_at)"," (또는 ",[22,381,382],{},"received_at",") — MySQL 8 파티셔닝 제약. ",[22,385,386],{},"DROP PARTITION","으로 회수, ",[22,389,390],{},"DELETE"," 금지.",[124,393,394,397,398,401,402,404],{},[27,395,396],{},"§2 Hot\u002FWarm\u002FCold"," — Hot(Aurora 90일) → Warm(Aurora 콜드 13개월) → ",[27,399,400],{},"Cold(R2 Parquet)",". ",[22,403,386],{}," 직전 Parquet 덤프 Worker Cron.",[124,406,407,414,415,417],{},[27,408,409,410,413],{},"§3 ",[22,411,412],{},"raw_payload"," R2 오프로드"," — 1 KB 미만은 인라인, 초과분은 ",[22,416,69],{},"로 분리. 평균 DB 부담 1\u002F10 수준.",[124,419,420,423,424,427,428,433],{},[27,421,422],{},"§4 사전 집계"," — 기존 ",[22,425,426],{},"TB_DISPATCH_STAT_DAILY"," 옆에 ",[27,429,430],{},[22,431,432],{},"TB_DISPATCH_STAT_HOURLY"," 추가 (5분 주기). 시간 범위별 출처 계층화.",[124,435,436,439],{},[27,437,438],{},"§5 인덱스·쿼리 가드"," — OFFSET 금지·커서 페이징·30일 기본 윈도우·JSON generated column.",[124,441,442,445,446,54,449,452],{},[27,443,444],{},"§6 Aurora 토폴로지"," — Writer\u002FReader 분리, Hyperdrive 바인딩 2개(",[22,447,448],{},"_W",[22,450,451],{},"_R","), Limitless 전환 기준.",[124,454,455,216,458,138,461,464],{},[27,456,457],{},"§7 운영 트리거",[22,459,460],{},"DISPATCH_ITEM > 5억 행 → ClickHouse PoC",[22,462,463],{},"Writer CPU 60%+ → Reader 추가"," 등 임계 룰북.",[14,466,468,469,119],{"id":467},"_4-초기-ddl-malgn-noti-apisrcdbmigrations0000_initialsql","4. 초기 DDL (",[22,470,471],{},"malgn-noti-api\u002Fsrc\u002Fdb\u002Fmigrations\u002F0000_initial.sql",[121,473,474,489,506,515,518],{},[124,475,476,478,479,138,482,485,486,142],{},[27,477,147],{}," — MySQL 8.0 \u002F Aurora MySQL 3 호환, ",[22,480,481],{},"utf8mb4_0900_ai_ci",[22,483,484],{},"ENGINE=InnoDB",", 모든 컬럼·테이블에 ",[22,487,488],{},"COMMENT",[124,490,491,494,495,498,499,502,503,505],{},[27,492,493],{},"파티션 적용"," — 5개 테이블, 2026-05~2027-06 (14개월) + ",[22,496,497],{},"pmax",". 매월 25일 ",[22,500,501],{},"REORGANIZE",", 매월 1일 ",[22,504,386],{},"(외부 Cron Worker).",[124,507,508,216,511,514],{},[27,509,510],{},"§13.3 R2 오프로드 컬럼",[22,512,513],{},"TB_DISPATCH_EVENT.raw_payload_r2_key VARCHAR(255) NULL"," 1차 스키마에 포함.",[124,516,517],{},"파티션 테이블은 MySQL 제약상 FK 미사용 → application-level 정합성, 주석 명시.",[124,519,520,523,524,54,527,54,530,533],{},[22,521,522],{},"drizzle.config.ts"," 신설 — ",[22,525,526],{},"db:introspect",[22,528,529],{},"db:generate",[22,531,532],{},"db:migrate"," 스크립트.",[14,535,537,538,541],{"id":536},"_5-hyperdrive-연결-malgn-noti-apiwranglertoml-신규-코드","5. Hyperdrive 연결 (",[22,539,540],{},"malgn-noti-api\u002Fwrangler.toml"," + 신규 코드)",[121,543,544,551,586,614,629],{},[124,545,546,547,142],{},"사용자 제공 Hyperdrive ID: ",[27,548,549],{},[22,550,76],{},[124,552,553,556,557],{},[22,554,555],{},"wrangler.toml",":\n",[558,559,564],"pre",{"className":560,"code":561,"language":562,"meta":563,"style":563},"language-toml shiki shiki-themes github-light github-dark","[[hyperdrive]]\nbinding = \"HYPERDRIVE\"\nid = \"a2ba4efe7421464da1d5ff5e620b33a3\"\n","toml","",[22,565,566,574,580],{"__ignoreMap":563},[567,568,571],"span",{"class":569,"line":570},"line",1,[567,572,573],{},"[[hyperdrive]]\n",[567,575,577],{"class":569,"line":576},2,[567,578,579],{},"binding = \"HYPERDRIVE\"\n",[567,581,583],{"class":569,"line":582},3,[567,584,585],{},"id = \"a2ba4efe7421464da1d5ff5e620b33a3\"\n",[124,587,588,216,593,368,595,401,598,601,602,605,606,609,610,613],{},[27,589,590],{},[22,591,592],{},"src\u002Fdb\u002Fclient.ts",[22,594,80],{},[22,596,597],{},"mysql2\u002Fpromise.createConnection",[22,599,600],{},"getDb(env, ctx)"," 요청 스코프 핸들(",[22,603,604],{},"ctx.waitUntil(conn.end())","), ",[22,607,608],{},"pingDb(env)"," 헬스. mysql2 mixin 타입 이슈는 ",[22,611,612],{},"db.execute(sql\\","...`)`로 우회.",[124,615,616,216,621,624,625,628],{},[27,617,618],{},[22,619,620],{},"src\u002Findex.ts",[22,622,623],{},"GET \u002Fhealth\u002Fdb"," 추가, ",[22,626,627],{},"Bindings.HYPERDRIVE: Hyperdrive"," 타입 결합 (cf-typegen).",[124,630,631,634,635,138,638,138,641,644,645,648],{},[27,632,633],{},"의존성",": ",[22,636,637],{},"drizzle-orm@0.36.4",[22,639,640],{},"mysql2@3.22.3",[22,642,643],{},"drizzle-kit@0.28.1",". wrangler ",[22,646,647],{},"4.90 → 4.94"," 업그레이드.",[14,650,652],{"id":651},"_6-로컬-개발-실제-hyperdrive","6. 로컬 개발 = 실제 Hyperdrive",[121,654,655,665,687],{},[124,656,657,658,661,662,664],{},"로컬 ",[22,659,660],{},"wrangler dev"," 기본 모드는 Hyperdrive를 로컬 Postgres로 에뮬레이트하려 하므로 실패 → ",[22,663,88],{}," 필요.",[124,666,667,668,671,672,674,675,195,678,681,682,686],{},"Hyperdrive는 4.94 시점에도 per-binding ",[22,669,670],{},"remote = true"," 미지원 (",[22,673,555],{},"에 메모만 남김) → ",[22,676,677],{},"package.json",[22,679,680],{},"dev"," 스크립트를 ",[27,683,684],{},[22,685,88],{}," 로 변경.",[124,688,689,692,693,696],{},[22,690,691],{},"pnpm dev"," 단독으로 ",[22,694,695],{},"http:\u002F\u002Flocalhost:8787"," 기동, 모든 요청이 실제 Cloudflare edge 경유 Hyperdrive → Aurora.",[14,698,700],{"id":699},"_7-프로덕션-배포","7. 프로덕션 배포",[121,702,703,715,726,737],{},[124,704,705,708,709,161,712,165],{},[22,706,707],{},"pnpm typecheck"," 통과 → ",[22,710,711],{},"pnpm run deploy",[22,713,714],{},"wrangler deploy",[124,716,717,718,722,723,142],{},"산출: ",[27,719,720],{},[22,721,94],{},", Version ",[22,724,725],{},"8b0d8674-57d0-4b00-966e-bdafc4de7a83",[124,727,728,729],{},"검증:\n",[558,730,735],{"className":731,"code":733,"language":734},[732],"language-text","GET \u002F             → 200 {\"name\":\"malgn-noti-api\",\"status\":\"placeholder\",\"env\":\"production\"}\nGET \u002Fhealth       → 200 {\"ok\":true,\"env\":\"production\"}\nGET \u002Fhealth\u002Fdb    → 200 {\"ok\":true,\"mysql_version\":\"8.0.42\"}   ← Aurora 응답\n","text",[22,736,733],{"__ignoreMap":563},[124,738,739,740,25,742,745],{},"의미 — ",[22,741,24],{},[27,743,744],{},"첫 프로덕션 배포","이자, Cloudflare Workers ↔ Hyperdrive ↔ AWS Aurora MySQL 경로가 살아 있음을 확인한 마일스톤.",[14,747,749],{"id":748},"_8-산출물","8. 산출물",[751,752,754,755,119],"h3",{"id":753},"신규-파일-malgn-noti-api","신규 파일 (",[22,756,757],{},"malgn-noti-api\u002F",[121,759,760,766,772,778,784,788,792],{},[124,761,762,765],{},[22,763,764],{},"doc\u002FDATA-MODEL.md"," (49 테이블 정본)",[124,767,768,771],{},[22,769,770],{},"doc\u002FERD.md"," (Mermaid 9 다이어그램)",[124,773,774,777],{},[22,775,776],{},"doc\u002FSCALABILITY.md"," (§13 상세 가이드)",[124,779,780,783],{},[22,781,782],{},"src\u002Fdb\u002Fmigrations\u002F0000_initial.sql"," (1049라인)",[124,785,786],{},[22,787,592],{},[124,789,790],{},[22,791,522],{},[124,793,794,797],{},[22,795,796],{},"worker-configuration.d.ts"," (cf-typegen 산출)",[751,799,801],{"id":800},"수정-파일","수정 파일",[121,803,804,809,816,825],{},[124,805,806,808],{},[22,807,555],{}," — Hyperdrive 바인딩 추가",[124,810,811,216,813,815],{},[22,812,620],{},[22,814,84],{}," 라우트 + 타입 확장",[124,817,818,820,821,824],{},[22,819,677],{}," — drizzle 의존성 + ",[22,822,823],{},"dev: wrangler dev --remote"," + db 스크립트",[124,826,827],{},[22,828,829],{},"pnpm-lock.yaml",[751,831,833],{"id":832},"커밋-malgn-noti-api","커밋 (malgn-noti-api)",[121,835,836,842,848],{},[124,837,838,841],{},[22,839,840],{},"eecf226"," — doc: 데이터 모델 \u002F ERD \u002F 확장성 전략 정리",[124,843,844,847],{},[22,845,846],{},"0653472"," — db: 초기 마이그레이션 0000_initial.sql + drizzle-kit 설정",[124,849,850,853],{},[22,851,852],{},"7a17504"," — Hyperdrive(MySQL) 연결 + \u002Fhealth\u002Fdb + Drizzle 런타임 셋업",[19,855,856,857,142],{},"푸시: ",[22,858,859],{},"decfaf0..7a17504 → origin\u002Fmain",[14,861,863,864,119],{"id":862},"_10-운영-컨벤션-명문화-malgn-noti-apiclaudemd-81","10. 운영 컨벤션 명문화 (",[22,865,102],{},[19,867,868,869,872,873,876,877,880],{},"배포 직후 사용자가 \"배포 규정은 ",[22,870,871],{},"malgn-noti\u002FCLAUDE.md"," 파일을 참고해 줘\"라고 명확히 짚어 — 이번 흐름이 우연이 아니라 ",[27,874,875],{},"명문 규정","으로 박혀야 다음에도 재현됨을 확인. ",[22,878,879],{},"malgn-noti-api\u002FCLAUDE.md","에 §8.1을 추가하여 다음을 정리:",[121,882,883,893,918,930],{},[124,884,885,888,889,892],{},[27,886,887],{},"Git"," — 단일 main, 사용자 명시 요청 시에만 커밋·푸시, 한국어 제목 + 본문 불릿 + ",[22,890,891],{},"Co-Authored-By: Claude Opus 4.7 (1M context) \u003Cnoreply@anthropic.com>"," trailer, 무관 untracked 파일 끌어들이지 않음.",[124,894,895,216,898,161,901,904,905,908,909,911,912,368,915,917],{},[27,896,897],{},"배포 (Workers)",[22,899,900],{},"pnpm typecheck → pnpm run deploy",[22,902,903],{},"pnpm deploy","는 pnpm 워크스페이스 명령과 충돌하므로 ",[22,906,907],{},"run"," 명시), 프로덕션 URL ",[22,910,94],{},", 검증은 ",[22,913,914],{},"\u002Fhealth",[22,916,84],{},"(mysql_version 반환), DDL\u002F시드는 Worker 배포와 분리 멱등 적용.",[124,919,920,216,923,925,926,929],{},[27,921,922],{},"작업 이력",[22,924,110],{},"가 ",[27,927,928],{},"3 레포 공통 정본","임을 명문화. API 변경도 같은 폴더의 그날 파일에 기록하며, 산출물 절에 별 레포 커밋 해시까지 함께 표기.",[124,931,932,933,925,935,937,938,941,942,624,944,946],{},"§8 개발 명령어 표 갱신 — ",[22,934,691],{},[22,936,88],{},"임을 반영, ",[22,939,940],{},"cf-typegen","·",[22,943,526],{},[22,945,711],{}," 명시.",[19,948,949,950,953,954,142],{},"산출물: ",[22,951,952],{},"malgn-noti-api: e09f70e docs: 배포·Git·작업 이력 운영 컨벤션 명문화 (§8.1)",". 푸시 ",[22,955,956],{},"7a17504..e09f70e → origin\u002Fmain",[14,958,960],{"id":959},"_11-malgn-noti-프론트-배포-53-새로고침-버튼-일괄-제거","11. malgn-noti 프론트 배포 #53 — 새로고침 버튼 일괄 제거",[19,962,963,964,967,968,971,972,975],{},"§7.1 흐름대로 ",[22,965,966],{},"pnpm build → npx wrangler@4 pages deploy dist --project-name=malgn-noti --branch=main --commit-dirty=true --commit-message \"Remove refresh buttons from list toolbars + update guide and DESIGN\""," 실행. ",[27,969,970],{},"Working tree에 누적된 사용자 작업분","(13개 파일)을 그대로 라이브로 올리고, 직후 ",[22,973,974],{},"52f653b"," 커밋으로 main을 라이브와 동기화.",[121,977,978,996,1007,1018,1032,1051],{},[124,979,980,981,988,989,138,992,995],{},"변경 패턴: 발송 조회·관리·연락처·발신정보·랜딩 등 ",[27,982,983,984,987],{},"목록 페이지의 ",[22,985,986],{},"list-toolbar"," 새로고침 버튼","과 보조 CSS(",[22,990,991],{},"toolbar-sep",[22,993,994],{},"toolbar-refresh",")를 일괄 제거. 페이지당 평균 −27~28라인.",[124,997,998,999,1002,1003,1006],{},"동시 변경: ",[22,1000,1001],{},"app\u002Fpages\u002Fguide.vue"," +162라인(가이드 확장), ",[22,1004,1005],{},"doc\u002FDESIGN.md"," 86라인 갱신.",[124,1008,1009,1010,1013,1014,1017],{},"빌드: Nitro ",[22,1011,1012],{},"cloudflare-pages"," 프리셋 → ",[22,1015,1016],{},"dist\u002F",", 총 2.96 MB \u002F gzip 889 KB.",[124,1019,1020,1021,1027,1028,142],{},"배포 URL: 프로덕션 ",[1022,1023,1024],"a",{"href":1024,"rel":1025},"https:\u002F\u002Fmalgn-noti.pages.dev",[1026],"nofollow",", alias ",[1022,1029,1030],{"href":1030,"rel":1031},"https:\u002F\u002F127705c3.malgn-noti.pages.dev",[1026],[124,1033,1034,1035,138,1037,138,1040,1043,1044,1047,1048,1050],{},"검증: ",[22,1036,54],{},[22,1038,1039],{},"\u002Fsender\u002Fnumbers",[22,1041,1042],{},"\u002Fhistory\u002Fsms"," 모두 HTTP 200. 새로고침 버튼 제거 마커 확인 — ",[22,1045,1046],{},"curl -s \u002Fsender\u002Fnumbers | grep -c toolbar-refresh"," → ",[22,1049,223],{}," (제거 확정).",[124,1052,949,1053,1056,1057,142],{},[22,1054,1055],{},"malgn-noti: 52f653b list 툴바 새로고침 버튼 일괄 제거 + guide \u002F DESIGN 갱신"," (13 files, +226 −297). 푸시 ",[22,1058,1059],{},"252033d..52f653b → origin\u002Fmain",[14,1061,1063],{"id":1062},"_12-aurora-mysql에-0000_initialsql-적용-49-테이블-75-파티션-라이브","12. Aurora MySQL에 0000_initial.sql 적용 — 49 테이블 + 75 파티션 라이브",[19,1065,1066,1067,1070],{},"Aurora가 SG로 Hyperdrive egress IP만 허용해 로컬 mysql CLI는 차단됨 → ",[27,1068,1069],{},"Worker 경유 마이그레이션"," 인프라를 구축하고 첫 DDL을 적용.",[751,1072,1074,1075,1078,1079,119],{"id":1073},"_121-admin-라우트-malgn-noti-apisrcroutesadmints","12.1 ",[22,1076,1077],{},"\u002Fadmin\u002F*"," 라우트 (",[22,1080,1081],{},"malgn-noti-api\u002Fsrc\u002Froutes\u002Fadmin.ts",[121,1083,1084,1112,1125,1134],{},[124,1085,1086,1089,1090,1092,1093,1096,1097,1100,1101,1104,1105,1108,1109,142],{},[27,1087,1088],{},"게이트",": 모든 ",[22,1091,1077],{},"는 ",[22,1094,1095],{},"X-Migrate-Token"," 헤더 = ",[22,1098,1099],{},"env.MIGRATE_TOKEN"," 일치 필수. 미설정 시 라우트 전체 403. 로컬은 ",[22,1102,1103],{},".dev.vars","(gitignored, ",[22,1106,1107],{},"openssl rand -hex 16","로 생성), 프로덕션은 ",[22,1110,1111],{},"wrangler secret put MIGRATE_TOKEN",[124,1113,1114,216,1117,1120,1121,1124],{},[22,1115,1116],{},"GET \u002Fadmin\u002Ftables",[22,1118,1119],{},"information_schema.TABLES","에서 ",[22,1122,1123],{},"TB_*"," 목록 조회.",[124,1126,1127,216,1130,1133],{},[22,1128,1129],{},"GET \u002Fadmin\u002Fpartitions",[22,1131,1132],{},"information_schema.PARTITIONS","에서 파티션 명세 조회.",[124,1135,1136,1139,1140,556,1143],{},[22,1137,1138],{},"POST \u002Fadmin\u002Fmigrate"," — body로 SQL 텍스트를 받아 ",[27,1141,1142],{},"statement 단위로 순차 실행",[121,1144,1145,1155,1161,1164],{},[124,1146,1147,1150,1151,1154],{},[22,1148,1149],{},"--"," 주석 제거 + ",[22,1152,1153],{},";"," 줄바꿈 기준 split.",[124,1156,1157,1158,1160],{},"이미 ",[22,1159,1123],{}," 테이블이 존재하면 409 거부(실수 방지).",[124,1162,1163],{},"첫 에러에서 중단(DDL 부분 적용 위험).",[124,1165,1166,1167,142],{},"응답: ",[22,1168,1169],{},"{ ok, statements_total, statements_succeeded, duration_ms, errors[] }",[751,1171,1173],{"id":1172},"_122-적용-절차","12.2 적용 절차",[19,1175,1176,1178,1179,1181],{},[22,1177,691],{},"(",[22,1180,88],{},")로 로컬 Worker가 실제 Hyperdrive → Aurora에 접근. curl로 SQL을 본문 전달:",[558,1183,1187],{"className":1184,"code":1185,"language":1186,"meta":563,"style":563},"language-bash shiki shiki-themes github-light github-dark","TOKEN=$(cat .dev.vars | cut -d= -f2)\ncurl -X POST http:\u002F\u002Flocalhost:8787\u002Fadmin\u002Fmigrate \\\n  -H \"X-Migrate-Token: $TOKEN\" \\\n  -H \"Content-Type: application\u002Fsql\" \\\n  --data-binary @src\u002Fdb\u002Fmigrations\u002F0000_initial.sql\n","bash",[22,1188,1189,1226,1243,1259,1269],{"__ignoreMap":563},[567,1190,1191,1195,1199,1202,1206,1210,1213,1216,1220,1223],{"class":569,"line":570},[567,1192,1194],{"class":1193},"sVt8B","TOKEN",[567,1196,1198],{"class":1197},"szBVR","=",[567,1200,1201],{"class":1193},"$(",[567,1203,1205],{"class":1204},"sScJk","cat",[567,1207,1209],{"class":1208},"sZZnC"," .dev.vars",[567,1211,1212],{"class":1197}," |",[567,1214,1215],{"class":1204}," cut",[567,1217,1219],{"class":1218},"sj4cs"," -d=",[567,1221,1222],{"class":1218}," -f2",[567,1224,1225],{"class":1193},")\n",[567,1227,1228,1231,1234,1237,1240],{"class":569,"line":576},[567,1229,1230],{"class":1204},"curl",[567,1232,1233],{"class":1218}," -X",[567,1235,1236],{"class":1208}," POST",[567,1238,1239],{"class":1208}," http:\u002F\u002Flocalhost:8787\u002Fadmin\u002Fmigrate",[567,1241,1242],{"class":1218}," \\\n",[567,1244,1245,1248,1251,1254,1257],{"class":569,"line":582},[567,1246,1247],{"class":1218},"  -H",[567,1249,1250],{"class":1208}," \"X-Migrate-Token: ",[567,1252,1253],{"class":1193},"$TOKEN",[567,1255,1256],{"class":1208},"\"",[567,1258,1242],{"class":1218},[567,1260,1262,1264,1267],{"class":569,"line":1261},4,[567,1263,1247],{"class":1218},[567,1265,1266],{"class":1208}," \"Content-Type: application\u002Fsql\"",[567,1268,1242],{"class":1218},[567,1270,1272,1275],{"class":569,"line":1271},5,[567,1273,1274],{"class":1218},"  --data-binary",[567,1276,1277],{"class":1208}," @src\u002Fdb\u002Fmigrations\u002F0000_initial.sql\n",[19,1279,1280],{},"응답:",[558,1282,1286],{"className":1283,"code":1284,"language":1285,"meta":563,"style":563},"language-json shiki shiki-themes github-light github-dark","{ \"ok\": true, \"statements_total\": 52, \"statements_succeeded\": 52, \"duration_ms\": 6684, \"errors\": [] }\n","json",[22,1287,1288],{"__ignoreMap":563},[567,1289,1290,1293,1296,1298,1301,1303,1306,1308,1311,1313,1316,1318,1320,1322,1325,1327,1330,1332,1335],{"class":569,"line":570},[567,1291,1292],{"class":1193},"{ ",[567,1294,1295],{"class":1218},"\"ok\"",[567,1297,634],{"class":1193},[567,1299,1300],{"class":1218},"true",[567,1302,138],{"class":1193},[567,1304,1305],{"class":1218},"\"statements_total\"",[567,1307,634],{"class":1193},[567,1309,1310],{"class":1218},"52",[567,1312,138],{"class":1193},[567,1314,1315],{"class":1218},"\"statements_succeeded\"",[567,1317,634],{"class":1193},[567,1319,1310],{"class":1218},[567,1321,138],{"class":1193},[567,1323,1324],{"class":1218},"\"duration_ms\"",[567,1326,634],{"class":1193},[567,1328,1329],{"class":1218},"6684",[567,1331,138],{"class":1193},[567,1333,1334],{"class":1218},"\"errors\"",[567,1336,1337],{"class":1193},": [] }\n",[751,1339,1341],{"id":1340},"_123-검증","12.3 검증",[121,1343,1344,1355,1380],{},[124,1345,1346,1047,1348,1354],{},[22,1347,1116],{},[27,1349,1350,1351,1353],{},"49개 ",[22,1352,1123],{}," 테이블"," 전부 생성 확인.",[124,1356,1357,1047,1359,161,1362,941,1364,941,1367,941,1370,941,1372,1374,1375,368,1378,165],{},[22,1358,1129],{},[27,1360,1361],{},"75개 파티션",[22,1363,164],{},[22,1365,1366],{},"TB_DISPATCH_ITEM",[22,1368,1369],{},"TB_DISPATCH_EVENT",[22,1371,371],{},[22,1373,374],{}," × 각 15 파티션 = ",[22,1376,1377],{},"p202605..p202706",[22,1379,497],{},[124,1381,1382],{},"적용 시간 6.7초.",[751,1384,1386],{"id":1385},"_124-산출물","12.4 산출물",[121,1388,1389,1395,1408],{},[124,1390,1391,1394],{},[22,1392,1393],{},"malgn-noti-api: a390f32 admin: \u002Fadmin\u002Fmigrate · \u002Fadmin\u002Ftables · \u002Fadmin\u002Fpartitions 라우트 추가"," (2 files, +163).",[124,1396,1397,1399,1400,1403,1404,1407],{},[22,1398,1103],{},"는 gitignored — ",[22,1401,1402],{},"MIGRATE_TOKEN","만 로컬 보관. 프로덕션 배포 시에는 별도로 ",[22,1405,1406],{},"wrangler secret put"," 필요(현 시점 미배포 — 라이브 Worker에는 admin 라우트 없음).",[124,1409,1410,1411,142],{},"푸시 ",[22,1412,1413],{},"e09f70e..a390f32 → origin\u002Fmain",[751,1415,1417,1418,1421],{"id":1416},"_125-결정-admin-라우트는-로컬-전용-선택-a-유지","12.5 결정 — admin 라우트는 ",[27,1419,1420],{},"로컬 전용"," (선택 A 유지)",[19,1423,1424,1425,1428],{},"향후 0001+ 마이그레이션도 동일 방식(",[22,1426,1427],{},"pnpm dev --remote"," + curl localhost)으로 적용. 프로덕션에는 배포하지 않음. 이유: 라우트가 공개 URL에 노출되면 토큰 유출 = DB 전체 권한 탈취 위험. 마이그레이션 빈도가 낮아 로컬 적용 부담이 작음. 잦아지면 그때 별도 admin-worker 분리 또는 GitHub Actions OIDC 등 더 안전한 방식으로 전환.",[751,1430,1432,1433,119],{"id":1431},"_126-환경-가드-추가-실수로도-프로덕션에-안-뚫리도록-63ba424","12.6 환경 가드 추가 — 실수로도 프로덕션에 안 뚫리도록 (",[22,1434,1435],{},"63ba424",[19,1437,1438,1439,1441,1442,1445],{},"토큰 게이트만으로도 보호되지만, 누군가 ",[22,1440,1111],{},"을 실수로 프로덕션에 등록하면 라우트가 살아남. 이걸 막는 ",[27,1443,1444],{},"이중 안전망",":",[558,1447,1451],{"className":1448,"code":1449,"language":1450,"meta":563,"style":563},"language-ts shiki shiki-themes github-light github-dark","admin.use('*', async (c, next) => {\n  if (c.env.APP_ENV !== 'local') {\n    return c.json({ code: 'not_found', message: 'Route not found' }, 404)\n  }\n  \u002F\u002F ... 토큰 검사 ...\n})\n","ts",[22,1452,1453,1491,1511,1541,1546,1552],{"__ignoreMap":563},[567,1454,1455,1458,1461,1463,1466,1468,1471,1473,1477,1479,1482,1485,1488],{"class":569,"line":570},[567,1456,1457],{"class":1193},"admin.",[567,1459,1460],{"class":1204},"use",[567,1462,1178],{"class":1193},[567,1464,1465],{"class":1208},"'*'",[567,1467,138],{"class":1193},[567,1469,1470],{"class":1197},"async",[567,1472,161],{"class":1193},[567,1474,1476],{"class":1475},"s4XuR","c",[567,1478,138],{"class":1193},[567,1480,1481],{"class":1475},"next",[567,1483,1484],{"class":1193},") ",[567,1486,1487],{"class":1197},"=>",[567,1489,1490],{"class":1193}," {\n",[567,1492,1493,1496,1499,1502,1505,1508],{"class":569,"line":576},[567,1494,1495],{"class":1197},"  if",[567,1497,1498],{"class":1193}," (c.env.",[567,1500,1501],{"class":1218},"APP_ENV",[567,1503,1504],{"class":1197}," !==",[567,1506,1507],{"class":1208}," 'local'",[567,1509,1510],{"class":1193},") {\n",[567,1512,1513,1516,1519,1521,1524,1527,1530,1533,1536,1539],{"class":569,"line":582},[567,1514,1515],{"class":1197},"    return",[567,1517,1518],{"class":1193}," c.",[567,1520,1285],{"class":1204},[567,1522,1523],{"class":1193},"({ code: ",[567,1525,1526],{"class":1208},"'not_found'",[567,1528,1529],{"class":1193},", message: ",[567,1531,1532],{"class":1208},"'Route not found'",[567,1534,1535],{"class":1193}," }, ",[567,1537,1538],{"class":1218},"404",[567,1540,1225],{"class":1193},[567,1542,1543],{"class":569,"line":1261},[567,1544,1545],{"class":1193},"  }\n",[567,1547,1548],{"class":569,"line":1271},[567,1549,1551],{"class":1550},"sJ8bj","  \u002F\u002F ... 토큰 검사 ...\n",[567,1553,1555],{"class":569,"line":1554},6,[567,1556,1557],{"class":1193},"})\n",[121,1559,1560,1568,1575,1581],{},[124,1561,657,1562,103,1564,1567],{},[22,1563,1103],{},[22,1565,1566],{},"APP_ENV=local"," 추가하여 오버라이드 (gitignored).",[124,1569,1570,1571,1574],{},"프로덕션 ",[22,1572,1573],{},"wrangler.toml [vars] APP_ENV=\"production\""," 유지 → 라우트가 무조건 404.",[124,1576,1577,1578,165],{},"외부에서 보면 \"라우트가 존재하지 않는 것\"처럼 위장 (",[22,1579,1580],{},"{\"code\":\"not_found\"}",[124,1582,728,1583],{},[121,1584,1585,1595,1603],{},[124,1586,1587,1590,1591,1594],{},[22,1588,1589],{},"localhost:8787\u002Fadmin\u002Ftables"," + 토큰 → ",[27,1592,1593],{},"200",", 49 tables.",[124,1596,1597,1599,1600,142],{},[22,1598,1589],{}," 토큰 누락 → ",[27,1601,1602],{},"403",[124,1604,1605,1608,1609,142],{},[22,1606,1607],{},"malgn-noti-api.malgnsoft.workers.dev\u002Fadmin\u002Ftables"," + 유효 토큰 → ",[27,1610,1538],{},[751,1612,1614,1615,138,1618,119],{"id":1613},"_127-pdf-erd를-인쇄용-pdf로-malgn-noti-apidocerdpdf-470b55a","12.7-pdf ERD를 인쇄용 PDF로 (",[22,1616,1617],{},"malgn-noti-api\u002Fdoc\u002FERD.pdf",[22,1619,1620],{},"470b55a",[19,1622,1623],{},"DDL과 동기화된 시각 ERD를 외부 공유·인쇄용 PDF로 생성:",[121,1625,1626,1632,1642,1645],{},[124,1627,1628,1631],{},[22,1629,1630],{},"@mermaid-js\u002Fmermaid-cli"," (mmdc)가 ERD.md의 9개 mermaid 코드블록을 페이지별 PDF로 렌더 (Chromium 1회 다운로드).",[124,1633,1634,1637,1638,1641],{},[22,1635,1636],{},"python3 -m pip install --user pypdf"," 후 ",[22,1639,1640],{},"PdfWriter.append","로 9 페이지를 1 파일(925 KB)로 병합.",[124,1643,1644],{},"각 페이지에서 한국어 텍스트 추출 검증 (테이블·컬럼·관계 라벨 모두 정상).",[124,1646,1647,1650],{},[22,1648,1649],{},"ERD.md §10","에 재생성 절차 명문화 — 다음 누군가 갱신할 때 막힘 없음.",[751,1652,1654,1655,161,1658,119],{"id":1653},"_127-마이그레이션-절차-정본-malgn-noti-apidocmigrationmd-47afe1a","12.7 마이그레이션 절차 정본 — ",[22,1656,1657],{},"malgn-noti-api\u002Fdoc\u002FMIGRATION.md",[22,1659,1660],{},"47afe1a",[19,1662,1663,1664,1667,1668,1671],{},"위 12.1~12.6의 결정·절차를 운영 문서 1개로 정리해서 정본화. 9개 섹션 — 왜 이런 절차인가(Aurora SG 제약), 사전 준비, SQL 작성 규칙, 적용 절차 step-by-step, 실패 처리 3안, FAQ 8건, 적용 이력 책임(git + history\u002F), 파티션 운영 분리, 관련 문서. ",[22,1665,1666],{},"CLAUDE.md §8","에 링크 추가, ",[22,1669,1670],{},"pnpm db:migrate","는 직접 연결 불가라 비현실적임을 표기.",[751,1673,1675],{"id":1674},"_128-다음-단계-마이그레이션-운영","12.8 다음 단계 (마이그레이션 운영)",[121,1677,1678,1691,1698],{},[124,1679,1680,1681,1684,1685,1687,1688,1690],{},"파티션 자동 운영 Cron Worker(",[22,1682,1683],{},"src\u002Fworkers\u002Fpartition-maintenance.ts",") — 매월 25일 다음 달 파티션 ",[22,1686,501],{},", 매월 1일 13개월 전 파티션 R2 덤프 + ",[22,1689,386],{}," (SCALABILITY §1·§2).",[124,1692,1693,1694,1697],{},"시드 데이터 ",[22,1695,1696],{},"0001_seed.sql"," (system terms, 샘플 템플릿 카탈로그).",[124,1699,1700,1703,1704,1707],{},[22,1701,1702],{},"pnpm db:introspect","로 ",[22,1705,1706],{},"src\u002Fdb\u002Fschema.ts"," 자동 생성.",[14,1709,1711,1712,138,1715,138,1718,119],{"id":1710},"_13-기본-crud-api-골격-hono-drizzle-zod-a146e81-8128468-2b6f720","13. 기본 CRUD API 골격 — Hono + Drizzle + Zod (",[22,1713,1714],{},"a146e81",[22,1716,1717],{},"8128468",[22,1719,1720],{},"2b6f720",[19,1722,1723,1724,1727],{},"49 테이블 전부 CRUD는 과하므로, ",[27,1725,1726],{},"재사용 가능한 인프라(errors\u002Fpagination\u002Fauth\u002Fschema) + 가장 활용도 높은 도메인(주소록·발신번호)을 패턴 사례로"," 완성. 나머지 도메인은 동일 패턴 복제.",[751,1729,1731,1732,119],{"id":1730},"_131-사전-픽스-a146e81","13.1 사전 픽스 (",[22,1733,1714],{},[121,1735,1736,1756],{},[124,1737,1738,925,1741,1743,1744,1747,1748,1751,1752,1755],{},[22,1739,1740],{},"getDb()",[22,1742,604],{},"을 즉시 등록해서 핸들러 사용 전에 연결이 닫히던 버그. 모든 라우트가 500 (",[22,1745,1746],{},"Can't add new command when connection is in closed state","). → ",[22,1749,1750],{},"conn.end()"," 호출 제거. Hyperdrive 풀링에 의존, isolate 종료시 GC. TODO: ",[22,1753,1754],{},"withDb"," 미들웨어로 finally + waitUntil 구조 리팩터.",[124,1757,1758,103,1761,1764],{},[22,1759,1760],{},"\u002Fadmin\u002Fmigrate",[22,1762,1763],{},"?allow_existing=1"," 쿼리 — 0001+ 마이그레이션 \u002F 시드 데이터 적용 시 TB_* 존재 가드 우회. 신규 DDL은 가드 유지.",[751,1766,1768,1769,119],{"id":1767},"_132-의존성-8128468","13.2 의존성 (",[22,1770,1717],{},[19,1772,1773,368,1776,1779,1780,1783,1784,1787],{},[22,1774,1775],{},"zod 4.4.3",[22,1777,1778],{},"@hono\u002Fzod-validator 0.8.0",". CLAUDE.md §9 \"모든 입력은 Zod로 파싱\" 규칙 준수. Zod v4는 ",[22,1781,1782],{},".partial()","이 ",[22,1785,1786],{},".refine()","된 스키마에서 동작하지 않으므로 베이스 스키마 공유 + refine 각각 적용 패턴 채택.",[751,1789,1791,1792,119],{"id":1790},"_133-인프라-라우트-2b6f720","13.3 인프라 + 라우트 (",[22,1793,1720],{},[19,1795,1796],{},[27,1797,1798],{},"재사용 가능 인프라",[121,1800,1801,1813,1826,1851],{},[124,1802,1803,216,1806,368,1809,1812],{},[22,1804,1805],{},"src\u002Flib\u002Ferrors.ts",[22,1807,1808],{},"AppError",[22,1810,1811],{},"errors"," 헬퍼(notFound\u002Fforbidden\u002Fconflict\u002Fvalidation 등).",[124,1814,1815,1818,1819,138,1822,1825],{},[22,1816,1817],{},"src\u002Flib\u002Fpagination.ts"," — 커서 페이징 (SCALABILITY §5) base64url JSON ",[22,1820,1821],{},"{ c: ISO, i: id }",[22,1823,1824],{},"paginate(rows, limit, toCursor)"," 헬퍼.",[124,1827,1828,216,1831,54,1834,54,1837,1840,1841,54,1844,54,1847,1850],{},[22,1829,1830],{},"src\u002Fmiddleware\u002Fauth.ts",[22,1832,1833],{},"requireAuth()",[22,1835,1836],{},"requireRole()",[22,1838,1839],{},"authCtx()",". 로컬 dev 단축(",[22,1842,1843],{},"X-Dev-Company-Id",[22,1845,1846],{},"X-Dev-User-Id",[22,1848,1849],{},"X-Dev-Role"," 헤더). 프로덕션 JWT는 signup\u002Flogin 라우트 구현 시 활성.",[124,1852,1853,1855,1856,138,1859,142],{},[22,1854,1706],{}," — Drizzle 수기 스키마 (touch한 6개 — TB_COMPANY, TB_USER, TB_CONTACT, TB_CONTACT_GROUP, TB_CONTACT_GROUP_MEMBER, TB_SENDER_PHONE). TS camelCase ↔ 물리 snake_case, ",[22,1857,1858],{},"status INT default 1",[22,1860,49],{},[19,1862,1863],{},[27,1864,1865],{},"도메인 라우트",[121,1867,1868,1874,1887,1897],{},[124,1869,1870,1873],{},[22,1871,1872],{},"GET \u002Fme"," — 현재 사용자 + 소속 고객사 (auth 검증용 최소).",[124,1875,1876,1879,1880,941,1883,1886],{},[22,1877,1878],{},"\u002Fcontacts"," — CRUD 완전체. list (커서·",[22,1881,1882],{},"?q=",[22,1884,1885],{},"?status=","), POST\u002FGET\u002FPATCH\u002FDELETE(soft).",[124,1888,1889,1892,1893,1896],{},[22,1890,1891],{},"\u002Fcontact-groups"," — CRUD + ",[22,1894,1895],{},"\u002F:id\u002Fmembers"," POST·DELETE (memberCount 캐시 자동 갱신, IN 절 + 소유 검증).",[124,1898,1899,1902,1903,1906],{},[22,1900,1901],{},"\u002Fsender-phones"," — 신청·조회·삭제. ",[22,1904,1905],{},"approval_state=대기"," 신청, 승인된 번호는 자가 삭제 금지(403).",[19,1908,1909],{},[27,1910,1911,1912,119],{},"전역 wiring (",[22,1913,620],{},[121,1915,1916,1928],{},[124,1917,1918,195,1920,1923,1924,1927],{},[22,1919,1808],{},[22,1921,1922],{},"onError"," 핸들러 → ",[22,1925,1926],{},"{ code, message, details? }"," 표준 응답.",[124,1929,1930,1931,941,1933,941,1935,1937],{},"4개 라우트 등록 + 기존 ",[22,1932,914],{},[22,1934,84],{},[22,1936,1077],{}," 유지.",[751,1939,1941,1942,1944],{"id":1940},"_134-검증-pnpm-dev-remote-curl","13.4 검증 (",[22,1943,1427],{}," + curl)",[558,1946,1948],{"className":1184,"code":1947,"language":1186,"meta":563,"style":563},"TOKEN=$(grep ^MIGRATE_TOKEN= .dev.vars | cut -d= -f2)\n# 시드\n{ echo 'INSERT IGNORE INTO TB_COMPANY (id, name, status) VALUES (1, \"테스트사\", 1);';\n  echo 'INSERT IGNORE INTO TB_USER (id, company_id, loginid, password_hash, name, role, status) VALUES (1, 1, \"admin@test.com\", \"stub\", \"테스트관리자\", \"admin\", 1);';\n} | curl -X POST \"http:\u002F\u002Flocalhost:8787\u002Fadmin\u002Fmigrate?allow_existing=1\" \\\n    -H \"X-Migrate-Token: $TOKEN\" -H \"Content-Type: application\u002Fsql\" --data-binary @-\n",[22,1949,1950,1976,1981,1994,2004,2024],{"__ignoreMap":563},[567,1951,1952,1954,1956,1958,1961,1964,1966,1968,1970,1972,1974],{"class":569,"line":570},[567,1953,1194],{"class":1193},[567,1955,1198],{"class":1197},[567,1957,1201],{"class":1193},[567,1959,1960],{"class":1204},"grep",[567,1962,1963],{"class":1208}," ^MIGRATE_TOKEN=",[567,1965,1209],{"class":1208},[567,1967,1212],{"class":1197},[567,1969,1215],{"class":1204},[567,1971,1219],{"class":1218},[567,1973,1222],{"class":1218},[567,1975,1225],{"class":1193},[567,1977,1978],{"class":569,"line":576},[567,1979,1980],{"class":1550},"# 시드\n",[567,1982,1983,1985,1988,1991],{"class":569,"line":582},[567,1984,1292],{"class":1193},[567,1986,1987],{"class":1218},"echo",[567,1989,1990],{"class":1208}," 'INSERT IGNORE INTO TB_COMPANY (id, name, status) VALUES (1, \"테스트사\", 1);'",[567,1992,1993],{"class":1193},";\n",[567,1995,1996,1999,2002],{"class":569,"line":1261},[567,1997,1998],{"class":1218},"  echo",[567,2000,2001],{"class":1208}," 'INSERT IGNORE INTO TB_USER (id, company_id, loginid, password_hash, name, role, status) VALUES (1, 1, \"admin@test.com\", \"stub\", \"테스트관리자\", \"admin\", 1);'",[567,2003,1993],{"class":1193},[567,2005,2006,2009,2012,2015,2017,2019,2022],{"class":569,"line":1271},[567,2007,2008],{"class":1193},"} ",[567,2010,2011],{"class":1197},"|",[567,2013,2014],{"class":1204}," curl",[567,2016,1233],{"class":1218},[567,2018,1236],{"class":1208},[567,2020,2021],{"class":1208}," \"http:\u002F\u002Flocalhost:8787\u002Fadmin\u002Fmigrate?allow_existing=1\"",[567,2023,1242],{"class":1218},[567,2025,2026,2029,2031,2033,2035,2038,2040,2043],{"class":569,"line":1554},[567,2027,2028],{"class":1218},"    -H",[567,2030,1250],{"class":1208},[567,2032,1253],{"class":1193},[567,2034,1256],{"class":1208},[567,2036,2037],{"class":1218}," -H",[567,2039,1266],{"class":1208},[567,2041,2042],{"class":1218}," --data-binary",[567,2044,2045],{"class":1208}," @-\n",[19,2047,2048],{},"확인된 동작:",[121,2050,2051,2054,2062,2071,2081,2090],{},[124,2052,2053],{},"401 미인증 \u002F 200 인증 \u002F 400 Zod 검증 실패 \u002F 404 미존재 모두 표준 응답",[124,2055,2056,2058,2059],{},[22,2057,1872],{}," → 200, ",[22,2060,2061],{},"{ user, company, ctxRole }",[124,2063,2064,2067,2068,119],{},[22,2065,2066],{},"POST \u002Fcontacts"," → 201, 한글·JSON 필드 정상 (",[22,2069,2070],{},"extraVars: {\"city\":\"서울\"}",[124,2072,2073,2076,2077,2080],{},[22,2074,2075],{},"GET \u002Fcontacts?limit=5"," → 커서 페이지, ",[22,2078,2079],{},"nextCursor: null"," (1건)",[124,2082,2083,2086,2087],{},[22,2084,2085],{},"POST \u002Fsender-phones"," → 201, ",[22,2088,2089],{},"approvalState: \"대기\"",[124,2091,2092,2095],{},[22,2093,2094],{},"GET \u002Fsender-phones?approvalState=대기"," → URL 인코딩 한글 필터 정상",[751,2097,2099],{"id":2098},"_135-다음-단계","13.5 다음 단계",[19,2101,2102],{},"동일 패턴 복제:",[121,2104,2105],{},[124,2106,2107,138,2110,2113,2114,2117,2118,2121,2122,138,2125,941,2128,142],{},[22,2108,2109],{},"\u002Foptout-entries",[22,2111,2112],{},"\u002Ftemplates","(채널별 spec), ",[22,2115,2116],{},"\u002Fhistory\u002F*","(read-only 조인 뷰), ",[22,2119,2120],{},"\u002Fsender-*","(브랜드·도메인·인증서), ",[22,2123,2124],{},"\u002Fcampaigns",[22,2126,2127],{},"\u002Fcharge",[22,2129,2130],{},"\u002Fcredit",[19,2132,2133],{},"코어 미흡 영역:",[121,2135,2136,2145,2150],{},[124,2137,2138,54,2141,2144],{},[22,2139,2140],{},"signup",[22,2142,2143],{},"login"," → JWT 검증 + auth 미들웨어 활성. 현재는 dev 단축만.",[124,2146,2147,2149],{},[22,2148,1754],{}," 미들웨어 (finally + waitUntil 패턴) — 현재는 conn auto-close 안 함.",[124,2151,2152,2153,2156,2157,2159],{},"Zod 검증 실패 응답 — 현재 ",[22,2154,2155],{},"@hono\u002Fzod-validator"," 기본 형식. ",[22,2158,1808],{}," 형식과 통일하려면 hook 등록 검토.",[14,2161,2163,2164,2167,2168,119],{"id":2162},"_14-api-문서-페이지-doc-scalar-ui-beef401","14. API 문서 페이지 — ",[22,2165,2166],{},"\u002Fdoc"," Scalar UI (",[22,2169,2170],{},"beef401",[751,2172,2174],{"id":2173},"_141-엔드포인트","14.1 엔드포인트",[121,2176,2177,2183],{},[124,2178,2179,2182],{},[22,2180,2181],{},"GET \u002Fdoc\u002Fopenapi.json"," — OpenAPI 3.1 스펙 (raw JSON, ~17 KB)",[124,2184,2185,2188],{},[22,2186,2187],{},"GET \u002Fdoc"," — Scalar API Reference UI (현대적 OpenAPI 뷰어, Swagger UI 대안)",[751,2190,2192,2193,119],{"id":2191},"_142-내용-srcopenapits","14.2 내용 (",[22,2194,2195],{},"src\u002Fopenapi.ts",[121,2197,2198,2219,2257,2276,2302],{},[124,2199,2200,216,2203,941,2205,941,2207,941,2210,941,2213,941,2216],{},[27,2201,2202],{},"10 paths \u002F 16 operations",[22,2204,914],{},[22,2206,84],{},[22,2208,2209],{},"\u002Fme",[22,2211,2212],{},"\u002Fcontacts(\u002F{id})",[22,2214,2215],{},"\u002Fcontact-groups(\u002F{id}, \u002F{id}\u002Fmembers)",[22,2217,2218],{},"\u002Fsender-phones(\u002F{id})",[124,2220,2221,216,2224,54,2227,54,2230,54,2233,54,2236,54,2239,54,2242,54,2245,54,2248,54,2251,54,2254],{},[27,2222,2223],{},"11 schemas",[22,2225,2226],{},"Contact",[22,2228,2229],{},"ContactCreate",[22,2231,2232],{},"ContactPatch",[22,2234,2235],{},"ContactGroup",[22,2237,2238],{},"ContactGroupCreate",[22,2240,2241],{},"ContactGroupPatch",[22,2243,2244],{},"MembersBody",[22,2246,2247],{},"SenderPhone",[22,2249,2250],{},"SenderPhoneCreate",[22,2252,2253],{},"Me",[22,2255,2256],{},"Error",[124,2258,2259,216,2262,54,2265,54,2268,2271,2272,2275],{},[27,2260,2261],{},"security schemes",[22,2263,2264],{},"DevCompanyId",[22,2266,2267],{},"DevUserId",[22,2269,2270],{},"DevRole","(로컬 dev API 키 방식) + ",[22,2273,2274],{},"BearerAuth","(프로덕션 JWT, 구현 시 활성)",[124,2277,2278,216,2281,54,2284,54,2287,401,2290,216,2293,54,2296,54,2299],{},[27,2279,2280],{},"공통 parameters",[22,2282,2283],{},"Cursor",[22,2285,2286],{},"Limit",[22,2288,2289],{},"IdPath",[27,2291,2292],{},"공통 responses",[22,2294,2295],{},"Unauthorized",[22,2297,2298],{},"NotFound",[22,2300,2301],{},"Validation",[124,2303,2304,2307,2308,2311],{},[27,2305,2306],{},"사용 가이드"," — 인증 방식·응답 형식·커서 페이징·멀티 테넌트 격리·관련 문서 링크 — ",[22,2309,2310],{},"info.description","에 명문화",[751,2313,2315],{"id":2314},"_143-설계-결정","14.3 설계 결정",[121,2317,2318,2324,2333],{},[124,2319,2320,2323],{},[27,2321,2322],{},"손으로 작성"," (zod-openapi 자동 생성 미사용). Zod v4 호환 우려 + 단순성 우선. 라우트가 안정화되면 자동 생성 마이그레이션 검토.",[124,2325,2326,2329,2330,2332],{},[27,2327,2328],{},"드리프트 위험",": 라우트 추가\u002F변경 시 ",[22,2331,2195],{},"도 함께 갱신 필요. PR 리뷰 체크리스트에 명시.",[124,2334,2335,2338],{},[27,2336,2337],{},"Scalar UI 선택"," — Swagger UI 대비 모던 디자인·다크모드·코드 샘플 자동 생성.",[751,2340,2342],{"id":2341},"_144-검증","14.4 검증",[558,2344,2347],{"className":2345,"code":2346,"language":734},[732],"GET \u002Fdoc\u002Fopenapi.json → 200, 17 KB, openapi 3.1.0\n  paths: 10, schemas: 11\nGET \u002Fdoc              → 200, text\u002Fhtml, scalar 마커 포함\n",[22,2348,2346],{"__ignoreMap":563},[19,2350,2351,2352,2356],{},"브라우저로 ",[1022,2353,2354],{"href":2354,"rel":2355},"http:\u002F\u002Flocalhost:8787\u002Fdoc",[1026]," 접속 → 좌측 사이드바 네비게이션 + 우측 인터랙티브 콜 패널.",[14,2358,2360],{"id":2359},"_15-malgn-noti-api-프로덕션-배포-2","15. malgn-noti-api 프로덕션 배포 #2",[19,2362,2363,2364,2367],{},"§7에서 적용한 첫 배포(",[22,2365,2366],{},"Version 8b0d8674",") 이후 누적된 변경(§10 운영 컨벤션 명문화 \u002F §12 admin 라우트 + Aurora DDL 적용 인프라 \u002F §13 기본 CRUD API + Drizzle 스키마 \u002F §14 \u002Fdoc Scalar UI)을 한 번에 라이브로.",[751,2369,2371,2372,2374],{"id":2370},"_151-배포-흐름-malgn-noti-apiclaudemd-81-준수","15.1 배포 흐름 (",[22,2373,102],{}," 준수)",[558,2376,2379],{"className":2377,"code":2378,"language":734},[732],"pnpm typecheck       → 통과\npnpm run deploy      → Cloudflare Workers\n검증                  → \u002Fhealth, \u002Fhealth\u002Fdb, \u002Fdoc, \u002Fdoc\u002Fopenapi.json, \u002Fme, \u002Fadmin\u002F* 7건\n커밋·푸시              → 이미 sync (이전 단계마다 push했음)\nhistory              → 본 절 추가\n",[22,2380,2378],{"__ignoreMap":563},[751,2382,2384],{"id":2383},"_152-배포-결과","15.2 배포 결과",[121,2386,2387,2395,2398],{},[124,2388,2389,634,2392],{},[27,2390,2391],{},"Version ID",[22,2393,2394],{},"1fdc3b12-9e43-4c31-90c4-609845569e65",[124,2396,2397],{},"번들: 2309.97 KiB \u002F gzip 549.75 KiB (이전 1638.71 KiB → 2309.97 KiB, drizzle + mysql2 + zod + Scalar 포함으로 증가)",[124,2399,2400],{},"Worker Startup: 49 ms",[751,2402,2404,2405,119],{"id":2403},"_153-검증-httpsmalgn-noti-apimalgnsoftworkersdev","15.3 검증 (",[1022,2406,94],{"href":94,"rel":2407},[1026],[2409,2410,2411,2424],"table",{},[2412,2413,2414],"thead",{},[2415,2416,2417,2421],"tr",{},[2418,2419,2420],"th",{},"엔드포인트",[2418,2422,2423],{},"결과",[2425,2426,2427,2441,2450,2461,2470,2479,2495],"tbody",{},[2415,2428,2429,2435],{},[2430,2431,2432],"td",{},[22,2433,2434],{},"GET \u002F",[2430,2436,2437,2438],{},"200, ",[22,2439,2440],{},"env: \"production\"",[2415,2442,2443,2448],{},[2430,2444,2445],{},[22,2446,2447],{},"GET \u002Fhealth",[2430,2449,1593],{},[2415,2451,2452,2456],{},[2430,2453,2454],{},[22,2455,623],{},[2430,2457,2437,2458],{},[22,2459,2460],{},"mysql_version: \"8.0.42\"",[2415,2462,2463,2467],{},[2430,2464,2465],{},[22,2466,2181],{},[2430,2468,2469],{},"200, 17 KB, 10 paths",[2415,2471,2472,2476],{},[2430,2473,2474],{},[22,2475,2187],{},[2430,2477,2478],{},"200, text\u002Fhtml (Scalar UI)",[2415,2480,2481,2486],{},[2430,2482,2483,2485],{},[22,2484,1872],{}," (인증 없음)",[2430,2487,2488,195,2491,2494],{},[27,2489,2490],{},"401",[22,2492,2493],{},"unauthenticated"," ← 인증 가드 작동",[2415,2496,2497,2502],{},[2430,2498,2499,2501],{},[22,2500,1116],{}," (유효 토큰)",[2430,2503,2504,195,2506,2509],{},[27,2505,1538],{},[22,2507,2508],{},"not_found"," ← env 가드(APP_ENV=production) 작동",[19,2511,2512],{},"두 가드 모두 프로덕션에서 정상 작동 — auth는 dev 헤더 거부 + JWT 미구현으로 401, admin은 토큰과 무관하게 404로 위장.",[751,2514,2516],{"id":2515},"_154-라이브-main-일치","15.4 라이브 ↔ main 일치",[19,2518,2519,2520,2523,2524,2526],{},"배포 시점 working tree와 ",[22,2521,2522],{},"main","이 이미 일치 (",[22,2525,2170],{},"). 추가 동기화 커밋 불필요.",[14,2528,2530,2531,119],{"id":2529},"_16-14개-도메인-라우트-추가-발신정보-주소록-템플릿-문의-결제-3bd9864","16. 14개 도메인 라우트 추가 — 발신정보 \u002F 주소록 \u002F 템플릿 \u002F 문의 \u002F 결제 (",[22,2532,2533],{},"3bd9864",[19,2535,2536],{},"§13의 4개 라우트(기본 CRUD 골격) 패턴을 복제해 나머지 주요 도메인을 일괄 확장. 49 테이블 중 ~24개를 API로 노출.",[751,2538,2540,2541,119],{"id":2539},"_161-스키마-확장-srcdbschemats","16.1 스키마 확장 (",[22,2542,1706],{},[19,2544,2545],{},"13개 Drizzle 테이블 추가:",[121,2547,2548,2569,2575,2584,2599],{},[124,2549,2550,2551,941,2554,941,2557,941,2560,941,2563,941,2566],{},"발신정보: ",[22,2552,2553],{},"rcsBrand",[22,2555,2556],{},"emailDomain",[22,2558,2559],{},"pushCert",[22,2561,2562],{},"kakaoProfileGroup",[22,2564,2565],{},"kakaoSenderProfile",[22,2567,2568],{},"optout080Number",[124,2570,2571,2572],{},"주소록: ",[22,2573,2574],{},"optoutEntry",[124,2576,2577,2578,941,2581],{},"템플릿: ",[22,2579,2580],{},"templateCategory",[22,2582,2583],{},"template",[124,2585,2586,2587,941,2590,941,2593,941,2596],{},"시스템: ",[22,2588,2589],{},"inquiry",[22,2591,2592],{},"inquiryReply",[22,2594,2595],{},"landingPage",[22,2597,2598],{},"companySettings",[124,2600,2601,2602,941,2605,2608],{},"결제: ",[22,2603,2604],{},"paymentMethod",[22,2606,2607],{},"creditLedger","(파티션 PK)",[751,2610,2612],{"id":2611},"_162-신규-라우트-14종","16.2 신규 라우트 14종",[2409,2614,2615,2628],{},[2412,2616,2617],{},[2415,2618,2619,2622,2625],{},[2418,2620,2621],{},"라우트",[2418,2623,2624],{},"메서드",[2418,2626,2627],{},"특징",[2425,2629,2630,2643,2655,2667,2679,2691,2703,2714,2726,2737,2753,2766,2778,2790],{},[2415,2631,2632,2637,2640],{},[2430,2633,2634],{},[22,2635,2636],{},"\u002Frcs-brands",[2430,2638,2639],{},"CRUD",[2430,2641,2642],{},"brandCode UNIQUE 409 처리",[2415,2644,2645,2650,2652],{},[2430,2646,2647],{},[22,2648,2649],{},"\u002Femail-domains",[2430,2651,2639],{},[2430,2653,2654],{},"verified_yn \u002F dkim_state 워크플로",[2415,2656,2657,2662,2664],{},[2430,2658,2659],{},[22,2660,2661],{},"\u002Fpush-certs",[2430,2663,2639],{},[2430,2665,2666],{},"credentialEnc 응답 제외, base64 입력",[2415,2668,2669,2674,2676],{},[2430,2670,2671],{},[22,2672,2673],{},"\u002Fkakao-profile-groups",[2430,2675,2639],{},[2430,2677,2678],{},"단순 그룹 메타",[2415,2680,2681,2686,2688],{},[2430,2682,2683],{},[22,2684,2685],{},"\u002Fkakao-sender-profiles",[2430,2687,2639],{},[2430,2689,2690],{},"sendKeyEnc 응답 제외, profileId UNIQUE",[2415,2692,2693,2698,2700],{},[2430,2694,2695],{},[22,2696,2697],{},"\u002Foptout-080-numbers",[2430,2699,2639],{},[2430,2701,2702],{},"line_state 워크플로",[2415,2704,2705,2709,2711],{},[2430,2706,2707],{},[22,2708,2109],{},[2430,2710,2639],{},[2430,2712,2713],{},"채널별, 발송 직전 핫 경로, status=-1로 거부 해제",[2415,2715,2716,2721,2723],{},[2430,2717,2718],{},[22,2719,2720],{},"\u002Ftemplate-categories",[2430,2722,2639],{},[2430,2724,2725],{},"트리(부모 검증), 자식 있으면 삭제 거부",[2415,2727,2728,2732,2734],{},[2430,2729,2730],{},[22,2731,2112],{},[2430,2733,2639],{},[2430,2735,2736],{},"채널 필터, 시스템 샘플(company_id NULL) 조회 포함, 수정 시 review_state→draft",[2415,2738,2739,2744,2750],{},[2430,2740,2741],{},[22,2742,2743],{},"\u002Finquiries",[2430,2745,2746,2747],{},"CRUD + ",[22,2748,2749],{},"\u002F:id\u002Freplies",[2430,2751,2752],{},"답변 추가 시 answer_state→progress",[2415,2754,2755,2760,2763],{},[2430,2756,2757],{},[22,2758,2759],{},"\u002Fcompany-settings",[2430,2761,2762],{},"GET \u002F PUT",[2430,2764,2765],{},"1:1 upsert (settings JSON)",[2415,2767,2768,2773,2775],{},[2430,2769,2770],{},[22,2771,2772],{},"\u002Fpayment-methods",[2430,2774,2639],{},[2430,2776,2777],{},"billingKeyEnc 마스킹, default_yn 단일성 보장",[2415,2779,2780,2785,2787],{},[2430,2781,2782],{},[22,2783,2784],{},"\u002Flanding-pages",[2430,2786,2639],{},[2430,2788,2789],{},"publishedYn=Y 시 publishedAt 자동",[2415,2791,2792,2797,2800],{},[2430,2793,2794],{},[22,2795,2796],{},"\u002Fcredit-ledger",[2430,2798,2799],{},"GET (read-only)",[2430,2801,2802],{},"append-only, entryType\u002F기간 필터",[751,2804,2806],{"id":2805},"_163-공통-패턴-기존-13과-동일","16.3 공통 패턴 (기존 §13과 동일)",[121,2808,2809,2818,2824,2830,2833],{},[124,2810,2811,2813,2814,2817],{},[22,2812,1833],{}," 미들웨어 + ",[22,2815,2816],{},"companyId"," 스코프",[124,2819,2820,2821],{},"커서 페이징 ",[22,2822,2823],{},"(created_at DESC, id DESC)",[124,2825,2826,2827],{},"soft delete ",[22,2828,2829],{},"status=-1",[124,2831,2832],{},"시크릿 필드(credentialEnc·sendKeyEnc·billingKeyEnc)는 응답에서 제외",[124,2834,2835],{},"UNIQUE 위반은 409 conflict 응답",[751,2837,2839],{"id":2838},"_164-검증-pnpm-dev-curl","16.4 검증 (pnpm dev + curl)",[19,2841,2842],{},"9개 라우트 POST\u002FGET 정상 동작 확인:",[121,2844,2845,2855,2860,2867],{},[124,2846,2847,138,2849,138,2851,138,2853],{},[22,2848,2636],{},[22,2850,2649],{},[22,2852,2109],{},[22,2854,2720],{},[124,2856,2857,2859],{},[22,2858,2112],{}," (한글 + JSON spec)",[124,2861,2862,138,2864,2866],{},[22,2863,2743],{},[22,2865,2759],{}," (GET\u002FPUT upsert)",[124,2868,2869,2871],{},[22,2870,2796],{}," (빈 목록 페이징)",[751,2873,2875],{"id":2874},"_165-미완료-알려진-한계","16.5 미완료 \u002F 알려진 한계",[121,2877,2878,2896,2903,2906],{},[124,2879,2880,2885,2886,2888,2889,2895],{},[27,2881,2882,2884],{},[22,2883,2166],{}," OpenAPI 스펙 동기화 미반영"," — 14개 신규 라우트가 ",[22,2887,2195],{},"에 없음. 손으로 작성 부담이 너무 큼. 다음 단계로 ",[27,2890,2891,2894],{},[22,2892,2893],{},"hono-openapi"," 자동 생성 마이그레이션"," 후 일괄 갱신 권장.",[124,2897,2898,2899,2902],{},"일부 PATCH 미제공 — sender-phones, kakao-profile-groups, optout-080-numbers, optout-entries 등 워크플로 상태 변경은 별도 RPC 라우트(",[22,2900,2901],{},"\u002Fsender-phones\u002F:id\u002Fapprove",")로 분리하는 게 깔끔.",[124,2904,2905],{},"인증·JWT는 여전히 dev 헤더만. signup\u002Flogin 구현이 다음 큰 마일스톤.",[124,2907,2908,2911],{},[22,2909,2910],{},"dispatch-*"," 라우트(발송 이력 read-only)는 파티션 테이블이라 별도 설계 필요. 다음 단계.",[14,2913,2915,2916,138,2919,138,2922,119],{"id":2914},"_17-phase-123-doc-동기화-인증-발송-이력-32f7ce4-c19f116-f45ad01","17. Phase 1·2·3 — \u002Fdoc 동기화 + 인증 + 발송 이력 (",[22,2917,2918],{},"32f7ce4",[22,2920,2921],{},"c19f116",[22,2923,2924],{},"f45ad01",[19,2926,2927],{},"§16 직후 사용자의 \"차례대로 진행\" 지시에 따라 3 phase 연속 진행.",[751,2929,2931,2932,119],{"id":2930},"_171-phase-1-openapits-확장-32f7ce4","17.1 Phase 1 — openapi.ts 확장 (",[22,2933,2918],{},[19,2935,2936,2937,2939],{},"§16에서 추가한 14개 라우트가 ",[22,2938,2166],{},"에 안 보이던 문제 해결.",[121,2941,2942,2948,2964],{},[124,2943,2944,2945,2947],{},"자동 생성 시도(",[22,2946,2893],{}," 1.x) → Standard Schema 기반 새 버전이 Zod v4 vendor 등록 필요로 복잡 → 롤백.",[124,2949,2950,2951,368,2954,941,2957,941,2960,2963],{},"대신 ",[27,2952,2953],{},"수기 확장",[22,2955,2956],{},"cursorList()",[22,2958,2959],{},"single()",[22,2961,2962],{},"ok"," 헬퍼로 응답 재사용해 간결화.",[124,2965,2966,2967,2970,2971,2974,2975,2978,2979,2982,2983,142],{},"규모: paths 10→",[27,2968,2969],{},"37",", operations 16→",[27,2972,2973],{},"71",", schemas 11→",[27,2976,2977],{},"45",", tags 5→",[27,2980,2981],{},"19",", 스펙 17 KB → ",[27,2984,2985],{},"54 KB",[751,2987,2989,2990,119],{"id":2988},"_172-phase-2-인증-signup-login-jwt-c19f116","17.2 Phase 2 — 인증 (signup \u002F login + JWT) (",[22,2991,2921],{},[19,2993,2994],{},"dev 헤더만으론 외부에서 호출 불가 → JWT 흐름 정식 구현.",[19,2996,2997],{},"신규 파일:",[121,2999,3000,3013,3023],{},[124,3001,3002,216,3005,3008,3009,3012],{},[22,3003,3004],{},"src\u002Flib\u002Fpassword.ts",[27,3006,3007],{},"PBKDF2-SHA256"," 100k 라운드 + salt 16B (Workers Web Crypto). 저장 형식 ",[22,3010,3011],{},"pbkdf2$\u003Citer>$\u003CsaltB64>$\u003ChashB64>",". timing-safe compare.",[124,3014,3015,3018,3019,3022],{},[22,3016,3017],{},"src\u002Flib\u002Fjwt.ts"," — HS256 (hono\u002Futils\u002Fjwt), payload ",[22,3020,3021],{},"{ sub, cid, role, iat, exp }",", 기본 만료 7일.",[124,3024,3025,3028],{},[22,3026,3027],{},"src\u002Froutes\u002Fauth.ts",[121,3029,3030,3036],{},[124,3031,3032,3035],{},[22,3033,3034],{},"POST \u002Fauth\u002Fsignup"," — 신규 고객사 + owner 사용자 생성, JWT 발급. loginid 중복 → 409. 사용자 생성 실패 시 고객사 best-effort 무효화.",[124,3037,3038,3041],{},[22,3039,3040],{},"POST \u002Fauth\u002Flogin"," — companyId + loginid + password 검증, JWT 발급. account enumeration 방지(일관된 401). lastLoginAt 갱신.",[19,3043,3044],{},"미들웨어:",[121,3046,3047],{},[124,3048,3049,216,3051,3054],{},[22,3050,1833],{},[27,3052,3053],{},"Bearer JWT 우선"," 검증 → 실패하면 dev 헤더(APP_ENV=local) 백업 → 둘 다 없으면 401.",[19,3056,3057],{},"설정:",[121,3059,3060],{},[124,3061,3062,3065,3066,3068,3069,165],{},[22,3063,3064],{},"JWT_SECRET"," 환경변수 추가 (",[22,3067,1103],{}," + 운영은 ",[22,3070,1406],{},[19,3072,3073],{},"검증 6건:",[121,3075,3076,3079,3082,3085,3088,3091],{},[124,3077,3078],{},"signup → 201 + JWT",[124,3080,3081],{},"GET \u002Fme with JWT → 200",[124,3083,3084],{},"login(정확) → 200 + JWT, lastLoginAt 갱신",[124,3086,3087],{},"login(틀린 pw) → 401",[124,3089,3090],{},"잘못된 JWT → 401",[124,3092,3093],{},"헤더 없음 → 401",[19,3095,3096],{},"알려진 한계:",[121,3098,3099,3102],{},[124,3100,3101],{},"OTP·약관·재설정·2FA·refresh token·세션 강제 로그아웃 미구현",[124,3103,3104],{},"고객사+사용자 생성 트랜잭션 미사용 (saga 권장)",[751,3106,3108,3109,119],{"id":3107},"_173-phase-3-발송-이력-read-only-f45ad01","17.3 Phase 3 — 발송 이력 read-only (",[22,3110,2924],{},[19,3112,3113],{},"발송 화면(history\u002F*.vue) 백엔드 API. 파티션 테이블이므로 시간 윈도우 강제.",[19,3115,3116],{},"스키마 추가:",[121,3118,3119,3130],{},[124,3120,3121,941,3124,3127,3128],{},[22,3122,3123],{},"dispatchRequest",[22,3125,3126],{},"dispatchItem"," 파티션 PK ",[22,3129,378],{},[124,3131,3132,3135,3136],{},[22,3133,3134],{},"dispatchStatDaily"," 복합 PK ",[22,3137,3138],{},"(companyId, channel, statDate)",[19,3140,3141,3142,3145],{},"신규 라우트 (",[22,3143,3144],{},"src\u002Froutes\u002Fdispatch-history.ts","):",[121,3147,3148,3158,3168,3177],{},[124,3149,3150,3153,3154,3157],{},[22,3151,3152],{},"GET \u002Fdispatch\u002Frequests"," — 기본 30일, ",[27,3155,3156],{},"max 90일"," 윈도우 강제, channel\u002FdispatchState 필터, 커서 페이징. window > 90일 → 400 Validation + \"Use Export job\" 힌트",[124,3159,3160,3163,3164,3167],{},[22,3161,3162],{},"GET \u002Fdispatch\u002Frequests\u002F:id?createdAt="," — 단건. ",[27,3165,3166],{},"createdAt 필수"," (파티션 pruning, SCALABILITY §1)",[124,3169,3170,3173,3174,3176],{},[22,3171,3172],{},"GET \u002Fdispatch\u002Frequests\u002F:id\u002Fitems"," — 수신자별 항목. ",[22,3175,2816],{}," 비정규화로 스코프 격리",[124,3178,3179,3182],{},[22,3180,3181],{},"GET \u002Fdispatch-stats"," — 일별 집계 (max 365일)",[19,3184,3185],{},"설계 (SCALABILITY §5 준수):",[121,3187,3188,3191,3198,3201],{},[124,3189,3190],{},"OFFSET 금지, 커서 페이징",[124,3192,3193,3194,3197],{},"단건 조회는 ",[22,3195,3196],{},"createdAt"," 명시 — 파티션 pruning 안 되면 전 파티션 스캔",[124,3199,3200],{},"시간 윈도우 강제로 사용자 실수에 의한 대량 스캔 차단",[124,3202,3203],{},"더 넓은 범위는 Export 잡(\u002Fexport-jobs)으로 우회 — 추후 추가",[19,3205,3206],{},"openapi.ts에 4 paths + 3 스키마(DispatchRequest\u002FItem\u002FStat) 추가. 최종 paths 41, operations 76, schemas 49.",[19,3208,3209],{},"검증:",[121,3211,3212,3215,3218],{},[124,3213,3214],{},"\u002Fdispatch\u002Frequests 기본 30일 → 200 empty + window",[124,3216,3217],{},"from-to 145일 → 400 validation + \"range too wide ... use Export job\"",[124,3219,3220],{},"\u002Fdispatch-stats 채널·기간 필터 → 200 empty + window",[14,3222,3224],{"id":3223},"_18-malgn-noti-api-프로덕션-배포-3-인증14-라우트발송-이력-라이브","18. malgn-noti-api 프로덕션 배포 #3 — 인증·14 라우트·발송 이력 라이브",[19,3226,3227],{},"§16·§17 누적 변경(14 도메인 라우트 + \u002Fauth + \u002Fdispatch + openapi 확장)을 라이브 반영.",[751,3229,3231],{"id":3230},"_181-사전","18.1 사전",[121,3233,3234,3243],{},[124,3235,3236,3238,3239,3242],{},[22,3237,3064],{}," wrangler secret 등록 — ",[22,3240,3241],{},"openssl rand -hex 32 | pnpm wrangler secret put JWT_SECRET"," (값은 로그 안 남김, 향후에도 노출 불가).",[124,3244,3245,3246,3248],{},"typecheck 통과, working tree clean (",[22,3247,2522],{},"이 모든 변경 이미 포함).",[751,3250,3252],{"id":3251},"_182-배포","18.2 배포",[121,3254,3255,3260,3266,3269],{},[124,3256,3257,3259],{},[22,3258,711],{}," (wrangler deploy)",[124,3261,3262,3263],{},"Version: ",[22,3264,3265],{},"926017d2-6ba8-440f-b405-5330ef3f2ffb",[124,3267,3268],{},"번들: 2439 KiB \u002F gzip 568 KiB (이전 #2 2310 → +130 KiB, auth + dispatch + openapi 확장)",[124,3270,3271],{},"Worker Startup 62 ms",[751,3273,3275],{"id":3274},"_183-검증-프로덕션-엔드투엔드-인증-흐름","18.3 검증 — 프로덕션 엔드투엔드 인증 흐름",[2409,3277,3278,3286],{},[2412,3279,3280],{},[2415,3281,3282,3284],{},[2418,3283,2420],{},[2418,3285,2423],{},[2425,3287,3288,3300,3311,3330,3341,3352,3365,3382],{},[2415,3289,3290,3294],{},[2430,3291,3292],{},[22,3293,2447],{},[2430,3295,3296,3297],{},"200 ",[22,3298,3299],{},"env: production",[2415,3301,3302,3306],{},[2430,3303,3304],{},[22,3305,623],{},[2430,3307,3296,3308],{},[22,3309,3310],{},"mysql_version: 8.0.42",[2415,3312,3313,3317],{},[2430,3314,3315],{},[22,3316,2181],{},[2430,3318,3319,3320,161,3323,941,3326,3329],{},"200, 61.9 KB, ",[27,3321,3322],{},"43 paths · 77 ops · 51 schemas",[22,3324,3325],{},"\u002Fauth\u002Fsignup",[22,3327,3328],{},"\u002Fauth\u002Flogin"," 포함)",[2415,3331,3332,3336],{},[2430,3333,3334,2501],{},[22,3335,1116],{},[2430,3337,3338,3340],{},[27,3339,1538],{}," ← env 가드 유지",[2415,3342,3343,3348],{},[2430,3344,3345,3347],{},[22,3346,1872],{}," (no auth)",[2430,3349,3350],{},[27,3351,2490],{},[2415,3353,3354,3359],{},[2430,3355,3356,3358],{},[22,3357,3034],{}," (실제 가입)",[2430,3360,3361,3364],{},[27,3362,3363],{},"201"," + JWT — companyId=4, \"프로덕션테스트\" 생성",[2415,3366,3367,3372],{},[2430,3368,3369,3371],{},[22,3370,1872],{}," with Bearer JWT",[2430,3373,3374,216,3376,138,3379],{},[27,3375,1593],{},[22,3377,3378],{},"user.role=owner",[22,3380,3381],{},"company.name=프로덕션테스트",[2415,3383,3384,3389],{},[2430,3385,3386,3388],{},[22,3387,3040],{}," (잘못된 pw)",[2430,3390,3391],{},[27,3392,2490],{},[19,3394,3395,3398],{},[27,3396,3397],{},"의미"," — 프로덕션 Worker가 실제 Aurora에 직접 INSERT\u002FSELECT 수행, JWT가 7일 만료로 발급, dev 헤더는 production에서 무시되어 보안 격리 유지.",[751,3400,3402],{"id":3401},"_184-라이브-main-일치","18.4 라이브 ↔ main 일치",[19,3404,2519,3405,1178,3407,3409],{},[22,3406,2522],{},[22,3408,2924],{},") 일치. 추가 동기화 커밋 불필요.",[751,3411,3413],{"id":3412},"_185-다음-단계","18.5 다음 단계",[19,3415,3416,3417,3420,3421,3424],{},"추천 2번 — ",[27,3418,3419],{},"POST \u002Fsend (발송 큐 producer)"," + NHN 어댑터. 발송 이력은 readonly 갖췄으니 쓰기 경로 차례.\n추천 3번 — Export 잡 (",[22,3422,3423],{},"\u002Fexport-jobs","): 90일 초과 이력 조회 우회.",[14,3426,3428,3429,119],{"id":3427},"_19-post-sendsms-발송-producer-db-적재까지-fb99b66","19. POST \u002Fsend\u002Fsms — 발송 producer (DB 적재까지) (",[22,3430,3431],{},"fb99b66",[751,3433,3435],{"id":3434},"_191-흐름","19.1 흐름",[3437,3438,3439,3445,3451,3454,3461,3464],"ol",{},[124,3440,3441,3444],{},[22,3442,3443],{},"Idempotency-Key"," 헤더 필수 (멱등성 — 현재 버그 있음, 19.4 참조)",[124,3446,3447,3448],{},"발신번호 검증 — 본인 고객사 + ",[22,3449,3450],{},"approval_state=승인",[124,3452,3453],{},"smsType 자동 추론(90B 초과→LMS, 첨부→MMS) 또는 명시",[124,3455,3456,3457,3460],{},"옵트아웃 필터 — ",[22,3458,3459],{},"TB_OPTOUT_ENTRY"," IN 절 lookup",[124,3462,3463],{},"단가 계산 (임시 단가표: SMS 9.9, LMS 30, MMS 100)",[124,3465,3466,3467,3469,3470],{},"트랜잭션 — 크레딧 조건부 차감 + 원장 hold + ",[22,3468,164],{}," + bulk ",[22,3471,3472],{},"TB_DISPATCH_ITEMs",[751,3474,3476],{"id":3475},"_192-신규-파일","19.2 신규 파일",[121,3478,3479,3490],{},[124,3480,3481,216,3484,368,3487],{},[22,3482,3483],{},"src\u002Flib\u002Fpricing.ts",[22,3485,3486],{},"SMS_PRICING",[22,3488,3489],{},"detectSmsType(body, hasAttachment)",[124,3491,3492,216,3495,3498],{},[22,3493,3494],{},"src\u002Froutes\u002Fsend.ts",[22,3496,3497],{},"POST \u002Fsend\u002Fsms",", 최대 1000 수신자\u002F요청",[751,3500,3502],{"id":3501},"_193-검증","19.3 검증",[121,3504,3505,3516,3519,3522,3525],{},[124,3506,3507,3508,3511,3512,3515],{},"정상 발송 → 201, ",[22,3509,3510],{},"recipientCount"," · ",[22,3513,3514],{},"totalCredit"," · 잔액 갱신 OK",[124,3517,3518],{},"옵트아웃 필터: 2명 중 1명만 발송 동작 확인",[124,3520,3521],{},"미승인 발신번호 → 403",[124,3523,3524],{},"잘못된 senderPhoneId → 404",[124,3526,3527],{},"크레딧 부족 (조건부 UPDATE affectedRows=0) → 409 conflict",[19,3529,3530,3531,941,3534,368,3537,3539],{},"openapi.ts: ",[22,3532,3533],{},"SendSmsRequest",[22,3535,3536],{},"SendResponse",[22,3538,3497],{},". 최종 paths 44, ops 78, schemas 53.",[751,3541,3543],{"id":3542},"_194-알려진-한계-idempotency-bug","19.4 알려진 한계 — IDEMPOTENCY BUG",[19,3545,3546,3547,3549,3550,3553],{},"같은 ",[22,3548,3443],{}," 연달아 호출 시 멱등 SELECT가 직전 INSERT를 못 보고 중복 적재됨. 두 번 호출에 ",[22,3551,3552],{},"dispatchRequestId"," 2개 생성, 크레딧 2번 차감 발생.",[121,3555,3556,3562,3565],{},[124,3557,3558,3559,3561],{},"Drizzle 일반 select \u002F ",[22,3560,612],{},"...`)` raw \u002F cache-bust 주석 모두 같은 동작",[124,3563,3564],{},"Hyperdrive read-cache 후보가 가장 유력했으나 raw + 주석에도 실패 → 다른 원인 가능",[124,3566,3567,3570,3571,3574,3575,3578],{},[27,3568,3569],{},"TODO(idempotency v2)"," — 별 ",[22,3572,3573],{},"TB_IDEMPOTENCY"," (비파티션, ",[22,3576,3577],{},"UNIQUE(company_id, key)",") 테이블로 INSERT-then-conflict 패턴 정식 구현. race-free + 캐시 무관. 코드에 상세 주석 남김.",[751,3580,3582],{"id":3581},"_195-다음-단계","19.5 다음 단계",[19,3584,3585],{},"본 라우트는 DB 적재까지만. 실제 NHN 발송까지:",[121,3587,3588,3597,3606,3615,3626],{},[124,3589,3590,216,3593,3596],{},[27,3591,3592],{},"Cloudflare Queues 설정",[22,3594,3595],{},"wrangler.toml [[queues.producers]]"," + 큐 생성",[124,3598,3599,216,3602,3605],{},[27,3600,3601],{},"Queue consumer worker",[22,3603,3604],{},"src\u002Fworkers\u002Fdispatch.ts"," (NHN 어댑터 호출)",[124,3607,3608,216,3611,3614],{},[27,3609,3610],{},"NHN SMS 어댑터",[22,3612,3613],{},"src\u002Fadapters\u002Fnhn\u002Fsms.ts"," (AppKey\u002FSecretKey 사용)",[124,3616,3617,216,3620,1047,3623,3625],{},[27,3618,3619],{},"Webhook handler",[22,3621,3622],{},"POST \u002Fwebhooks\u002Fnhn",[22,3624,1369],{}," 적재",[124,3627,3628,216,3631,138,3634,138,3637,138,3640,138,3643],{},[27,3629,3630],{},"다른 채널",[22,3632,3633],{},"\u002Fsend\u002Frcs",[22,3635,3636],{},"\u002Fsend\u002Fkakao",[22,3638,3639],{},"\u002Fsend\u002Femail",[22,3641,3642],{},"\u002Fsend\u002Fpush",[22,3644,3645],{},"\u002Fsend\u002Fflow",[14,3647,3649],{"id":3648},"_20-malgn-noti-api-프로덕션-배포-4-sendsms-발송-producer-라이브","20. malgn-noti-api 프로덕션 배포 #4 — \u002Fsend\u002Fsms 발송 producer 라이브",[19,3651,3652,3653,3655],{},"§19 발송 producer(",[22,3654,3431],{},")를 라이브 반영.",[751,3657,3659],{"id":3658},"_201-배포","20.1 배포",[121,3661,3662,3667,3670],{},[124,3663,3262,3664],{},[22,3665,3666],{},"4d9e1fbe-c8c5-4b70-933d-a48196fc2599",[124,3668,3669],{},"번들: 2450 KiB \u002F gzip 571 KiB (#3 2439 → 2450, send 라우트 + openapi 추가분 +11 KB)",[124,3671,3672],{},"Worker Startup 70 ms",[751,3674,3676,3677,119],{"id":3675},"_202-검증-httpsmalgn-noti-apimalgnsoftworkersdev","20.2 검증 (",[1022,3678,94],{"href":94,"rel":3679},[1026],[2409,3681,3682,3690],{},[2412,3683,3684],{},[2415,3685,3686,3688],{},[2418,3687,2420],{},[2418,3689,2423],{},[2425,3691,3692,3702,3712,3727,3736,3744,3756,3767,3782],{},[2415,3693,3694,3698],{},[2430,3695,3696],{},[22,3697,2447],{},[2430,3699,3296,3700],{},[22,3701,3299],{},[2415,3703,3704,3708],{},[2430,3705,3706],{},[22,3707,623],{},[2430,3709,3296,3710],{},[22,3711,3310],{},[2415,3713,3714,3718],{},[2430,3715,3716],{},[22,3717,2181],{},[2430,3719,3720,3721,161,3724,3329],{},"200, 65 KB, ",[27,3722,3723],{},"paths 44 · ops 78 · schemas 53",[22,3725,3726],{},"\u002Fsend\u002Fsms",[2415,3728,3729,3733],{},[2430,3730,3731,2501],{},[22,3732,1116],{},[2430,3734,3735],{},"404 ← env 가드",[2415,3737,3738,3742],{},[2430,3739,3740,3347],{},[22,3741,1872],{},[2430,3743,2490],{},[2415,3745,3746,3753],{},[2430,3747,3748,161,3750,119],{},[22,3749,3040],{},[22,3751,3752],{},"prod-1@test.com",[2430,3754,3755],{},"200 + JWT",[2415,3757,3758,3764],{},[2430,3759,3760,3763],{},[22,3761,3762],{},"GET \u002Fsender-phones"," with JWT",[2430,3765,3766],{},"200 빈 결과 (tenant 격리 정상)",[2415,3768,3769,3774],{},[2430,3770,3771,3773],{},[22,3772,3497],{}," (잘못된 senderPhoneId)",[2430,3775,3776,195,3778,3781],{},[27,3777,1538],{},[22,3779,3780],{},"sender_phone not found"," ← 검증 흐름 동작",[2415,3783,3784,3789],{},[2430,3785,3786,3788],{},[22,3787,3497],{}," (Idempotency-Key 누락)",[2430,3790,3791,195,3794,3797],{},[27,3792,3793],{},"400",[22,3795,3796],{},"Idempotency-Key 헤더 필수"," ← 헤더 가드 동작",[751,3799,3801],{"id":3800},"_203-의미","20.3 의미",[121,3803,3804,3807,3810],{},[124,3805,3806],{},"\u002Fsend\u002Fsms 라우트가 프로덕션 Aurora에 도달하여 검증·격리·에러 응답 모두 정상.",[124,3808,3809],{},"발신번호·옵트아웃·크레딧 hold·트랜잭션 등 코드 경로는 로컬에서 검증 완료, 프로덕션은 발신번호 시드가 없어 실 발송까지는 미테스트 (시드 적용 후 가능).",[124,3811,3812],{},"프로덕션 admin\u002Fmigrate가 404 차단이라 시드는 로컬 dev 경유로만 — 향후 신청·승인 흐름 도입 시 자연스레 해결.",[751,3814,3816],{"id":3815},"_204-라이브-main-일치","20.4 라이브 ↔ main 일치",[19,3818,2519,3819,1178,3821,3409],{},[22,3820,2522],{},[22,3822,3431],{},[751,3824,3826],{"id":3825},"_205-다음-단계-변경-없음","20.5 다음 단계 (변경 없음)",[3437,3828,3829,3835,3838,3841],{},[124,3830,3831,3832,3834],{},"🐛 멱등 버그 수정 — ",[22,3833,3573],{}," + INSERT-then-conflict",[124,3836,3837],{},"NHN SMS 어댑터 + Queues + consumer worker (실 발송)",[124,3839,3840],{},"다른 채널 send (RCS\u002FKakao\u002FEmail\u002FPush\u002FFlow)",[124,3842,3843],{},"Export 잡 — 90일 초과 이력 우회",[14,3845,3847],{"id":3846},"_21-재배포-5-코드-변경-없는-새-version-발급","21. 재배포 #5 — 코드 변경 없는 새 Version 발급",[19,3849,3850,3851,3854,3855,3857,3858,103,3860,3863],{},"§20 배포 직후 ",[22,3852,3853],{},"git push"," 단계에서 도구 권한 오류로 history 커밋이 중단됨. 다음 세션에서 사용자가 \"배포\"를 재실행하여 finalize. API 코드는 ",[22,3856,3431],{}," 그대로 (linter가 ",[22,3859,3017],{},[22,3861,3862],{},"as unknown as JwtPayload"," 캐스트 명시화 — HEAD에 이미 반영).",[121,3865,3866,3871,3874],{},[124,3867,3262,3868],{},[22,3869,3870],{},"afaa4c89-999e-4c0d-832e-3aef96acc326",[124,3872,3873],{},"같은 번들(2450 KiB \u002F gzip 571), Worker Startup 60 ms",[124,3875,1034,3876,3511,3878,3880,3881,3883,3884,3886],{},[22,3877,914],{},[22,3879,84],{}," (mysql 8.0.42) · ",[22,3882,2166],{}," (paths 44 · ops 78 · schemas 53) · ",[22,3885,3726],{}," 미인증 401 — 4건 모두 정상.",[19,3888,3889,3890,3892],{},"라이브 ↔ main: ",[22,3891,3431],{},"로 일치.",[14,3894,3896,3897,119],{"id":3895},"_22-멱등-버그-해결-tb_idempotency-insert-then-conflict-020307f","22. 🐛 멱등 버그 해결 — TB_IDEMPOTENCY + INSERT-then-conflict (",[22,3898,3899],{},"020307f",[19,3901,3902],{},"§19·§21에서 추적했던 발송 멱등 버그(같은 Idempotency-Key 재호출 시 중복 적재) 정식 수정.",[751,3904,3906],{"id":3905},"_221-해결-패턴","22.1 해결 패턴",[121,3908,3909,3929,3935],{},[124,3910,3911,3914,3915,3917,3918,368,3920,368,3923,138,3926,119],{},[27,3912,3913],{},"0001_idempotency.sql"," — 비파티션 추적 테이블 ",[22,3916,3573],{}," (PK ",[22,3919,37],{},[22,3921,3922],{},"scope",[22,3924,3925],{},"idempotency_key",[22,3927,3928],{},"result_id NULL→채움",[124,3930,3931,3934],{},[27,3932,3933],{},"race-free",": MySQL이 PK 인덱스로 atomic dedup. 진행 중 트랜잭션과는 row-lock 대기 후 duplicate key error.",[124,3936,3937,556,3940],{},[27,3938,3939],{},"\u002Fsend\u002Fsms 신규 흐름",[3437,3941,3942,3948,3958,3965,3972],{},[124,3943,3944,3947],{},[22,3945,3946],{},"INSERT TB_IDEMPOTENCY (resultType='pending')"," — 점유 시도",[124,3949,3950,3951,3954,3955,3957],{},"중복키 에러 → 다른 요청이 owner. ",[22,3952,3953],{},"result_id","로 기존 ",[22,3956,164],{}," 반환 (idempotent:true)",[124,3959,3960,3961,3964],{},"점유 성공 → 검증·트랜잭션 진행, 마지막에 ",[22,3962,3963],{},"UPDATE result_id"," 로 매핑",[124,3966,3967,3968,3971],{},"트랜잭션 실패 시 ",[22,3969,3970],{},"rollbackIdempotency()"," — 키 해제(재시도 가능)",[124,3973,3974,3975,3978],{},"진행 중인 요청이 commit 전인 경우 ",[22,3976,3977],{},"202 idempotent_in_flight"," 응답",[751,3980,3982],{"id":3981},"_222-적용-검증","22.2 적용 + 검증",[121,3984,3985,3995],{},[124,3986,3987,3988,3990,3991,3994],{},"Aurora에 ",[22,3989,3913],{}," 적용: ",[22,3992,3993],{},"count=50"," (TB_IDEMPOTENCY 신규)",[124,3996,3997,3998],{},"pnpm dev 검증:\n",[121,3999,4000,4003,4013],{},[124,4001,4002],{},"call 1 (key=K) → dispatchRequestId=8, idempotent:false",[124,4004,4005,4006,4009,4010,4012],{},"call 2 (key=K) → dispatchRequestId=",[27,4007,4008],{},"8"," (같음), idempotent:",[27,4011,1300],{}," ✅",[124,4014,4015],{},"call 3 (key=K2) → dispatchRequestId=9 (새), idempotent:false ✅",[751,4017,4019],{"id":4018},"_223-부속-변경","22.3 부속 변경",[121,4021,4022,4030,4041],{},[124,4023,4024,216,4026,4029],{},[22,4025,1706],{},[22,4027,4028],{},"idempotency"," 테이블 정의 추가",[124,4031,4032,216,4034,4036,4037,4040],{},[22,4033,2195],{},[22,4035,3726],{}," description의 TODO 문구 제거 + 202 ",[22,4038,4039],{},"idempotent_in_flight"," 응답 추가",[124,4042,4043,4045,4046,4049,4050,4052],{},[22,4044,3017],{}," — linter가 ",[22,4047,4048],{},"verifyJwt"," 캐스트를 ",[22,4051,3862],{},"로 명시화",[751,4054,4056],{"id":4055},"_224-다음-단계-변동-없음","22.4 다음 단계 (변동 없음)",[3437,4058,4059,4062,4065,4067],{},[124,4060,4061],{},"✅ 멱등 버그 — 완료",[124,4063,4064],{},"NHN SMS 어댑터 + Cloudflare Queues + consumer worker (실 발송)",[124,4066,3840],{},[124,4068,3843],{},[14,4070,4072],{"id":4071},"_23-malgn-noti-api-프로덕션-배포-6-queues-producer-consumer-라이브","23. malgn-noti-api 프로덕션 배포 #6 — Queues Producer + Consumer 라이브",[19,4074,4075,4076,138,4078,4081],{},"§22 멱등 수정 + NHN 어댑터 + Queues 일체(",[22,4077,3899],{},[22,4079,4080],{},"5e1ac72",") 라이브 반영.",[751,4083,4085],{"id":4084},"_231-배포","23.1 배포",[121,4087,4088,4093,4096,4099],{},[124,4089,3262,4090],{},[22,4091,4092],{},"b30dc2a3-dc5a-4050-a435-c3d03a5e69a7",[124,4094,4095],{},"번들: 2460 KiB \u002F gzip 572 KiB",[124,4097,4098],{},"Worker Startup 74 ms",[124,4100,4101,556,4104],{},[27,4102,4103],{},"신규 바인딩 라이브",[121,4105,4106,4115],{},[124,4107,4108,161,4111,4114],{},[22,4109,4110],{},"env.DISPATCH_QUEUE",[22,4112,4113],{},"malgn-noti-dispatch",") — Producer + Consumer 동시",[124,4116,4117,4120,4121,4124],{},[22,4118,4119],{},"env.NHN_MOCK"," secret = ",[22,4122,4123],{},"\"1\""," (모의 모드)",[751,4126,4128],{"id":4127},"_232-검증","23.2 검증",[2409,4130,4131,4139],{},[2412,4132,4133],{},[2415,4134,4135,4137],{},[2418,4136,2420],{},[2418,4138,2423],{},[2425,4140,4141,4151,4161,4170,4181],{},[2415,4142,4143,4147],{},[2430,4144,4145],{},[22,4146,2447],{},[2430,4148,3296,4149],{},[22,4150,3299],{},[2415,4152,4153,4157],{},[2430,4154,4155],{},[22,4156,623],{},[2430,4158,3296,4159],{},[22,4160,3310],{},[2415,4162,4163,4167],{},[2430,4164,4165],{},[22,4166,2181],{},[2430,4168,4169],{},"200, 65.3 KB, paths 44 · ops 78 · schemas 53",[2415,4171,4172,4176],{},[2430,4173,4174,3347],{},[22,4175,3497],{},[2430,4177,4178,4179],{},"401 ",[22,4180,2493],{},[2415,4182,4183,4189],{},[2430,4184,4185,161,4187,119],{},[22,4186,3040],{},[22,4188,3752],{},[2430,4190,3755],{},[19,4192,4193,4194,368,4197,4200],{},"배포 명세 — ",[22,4195,4196],{},"Producer for malgn-noti-dispatch",[22,4198,4199],{},"Consumer for malgn-noti-dispatch"," 동시 등록 확인.",[751,4202,4204],{"id":4203},"_233-큐-end-to-end-검증-보류","23.3 큐 end-to-end 검증 — 보류",[19,4206,4207,4210,4211,941,4214,4217],{},[27,4208,4209],{},"원인",": Cloudflare 원격 미리보기 인프라 장애 (1105 Temporarily unavailable, Ray ID ",[22,4212,4213],{},"a02212096ea185af",[22,4215,4216],{},"a02213318ecb85af"," 등). 30분간 지속.",[121,4219,4220,4225],{},[124,4221,657,4222,4224],{},[22,4223,1427],{},"가 Cloudflare edge-preview 토큰을 받아오는 단계에서 503 → 시드 SQL POST가 모두 1105 응답",[124,4226,4227,4228,4231,4232,4235,4236,4238,4239,4241],{},"결과: 프로덕션 company 4에 ",[22,4229,4230],{},"sender_phone"," (승인) + ",[22,4233,4234],{},"credit_balance"," 시드 불가 → ",[22,4237,3726],{}," 호출이 404 ",[22,4240,3780],{},"로 종료",[19,4243,4244],{},"코드·바인딩 자체는 정상 등록됐고, 큐 처리 흐름은 Cloudflare 회복 후 재검증 예정. 검증 절차:",[3437,4246,4247,4252,4258,4264,4269,4288],{},[124,4248,4249,4251],{},[22,4250,1427],{}," (인프라 회복 후)",[124,4253,4254,4257],{},[22,4255,4256],{},"\u002Fadmin\u002Fmigrate?allow_existing=1"," 로 company 4 시드",[124,4259,4260,4261,4263],{},"PROD URL ",[22,4262,3328],{}," → JWT",[124,4265,4260,4266,4268],{},[22,4267,3726],{}," (senderPhoneId=100)",[124,4270,4271,4272,4275,4276,4278,4279,1047,4282,1047,4285],{},"5~10초 대기 후 PROD ",[22,4273,4274],{},"\u002Fdispatch\u002Frequests\u002F:id"," 로 ",[22,4277,243],{}," 천이 추적: ",[22,4280,4281],{},"queued",[22,4283,4284],{},"sending",[22,4286,4287],{},"delivered",[124,4289,4290,4293,4294,138,4297,4300],{},[22,4291,4292],{},"\u002Fdispatch\u002Frequests\u002F:id\u002Fitems"," 에서 ",[22,4295,4296],{},"send_state=sent",[22,4298,4299],{},"nhn_request_id=mock-..."," 확인",[751,4302,4304],{"id":4303},"_234-라이브-main-일치","23.4 라이브 ↔ main 일치",[19,4306,2519,4307,1178,4309,4311],{},[22,4308,2522],{},[22,4310,4080],{},") 일치.",[14,4313,4315],{"id":4314},"_24-다음-단계-알려진-한계","24. 다음 단계 \u002F 알려진 한계",[121,4317,4318,4328,4336,4343,4354,4364,4384],{},[124,4319,4320,4323,4324,4327],{},[27,4321,4322],{},"DDL 적용"," — Hyperdrive 콘솔은 자격증명만 보유. Aurora 측에 ",[22,4325,4326],{},"0000_initial.sql","을 적용해야 실제 테이블 생성. MySQL CLI 또는 Bastion 경유.",[124,4329,4330,216,4333,4335],{},[27,4331,4332],{},"파티션 자동 운영 Cron Worker",[22,4334,1683],{}," (월 1일 DROP + 25일 REORGANIZE).",[124,4337,4338,216,4341,1697],{},[27,4339,4340],{},"시드 데이터",[22,4342,1696],{},[124,4344,4345,4348,4349,1703,4351,4353],{},[27,4346,4347],{},"Drizzle 스키마 자동 생성"," — DDL 적용 후 ",[22,4350,1702],{},[22,4352,1706],{}," 생성, 카멜케이스 객체명 정리.",[124,4355,4356,4359,4360,4363],{},[27,4357,4358],{},"Reader 분리"," — 트래픽 증가 시 별도 Reader Hyperdrive 추가, ",[22,4361,4362],{},"HYPERDRIVE_R"," 바인딩 (SCALABILITY §6).",[124,4365,4366,216,4374,4376,4377,54,4380,4383],{},[27,4367,4368,54,4371],{},[22,4369,4370],{},"before",[22,4372,4373],{},"after",[22,4375,374],{},"는 DDL에서 MySQL 예약어 충돌 회피 차 ",[22,4378,4379],{},"before_json",[22,4381,4382],{},"after_json","으로 명명. DATA-MODEL.md 본문 표기와 다음 동기화 시 일치 필요.",[124,4385,4386,4389,4390,4393,4394,1120,4396,4399],{},[27,4387,4388],{},"로컬 MySQL 옵션"," — 오프라인 개발이 필요해지면 ",[22,4391,4392],{},"localConnectionString = \"mysql:\u002F\u002F...\""," 추가하고 ",[22,4395,680],{},[22,4397,4398],{},"--remote"," 제거.",[4401,4402,4403],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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}",{"title":563,"searchDepth":582,"depth":582,"links":4405},[4406,4407,4409,4411,4413,4415,4417,4418,4419,4425,4427,4428,4444,4456,4464,4472,4481,4490,4497,4505,4513,4514,4521,4527],{"id":16,"depth":576,"text":17},{"id":114,"depth":576,"text":4408},"1. 데이터 모델 (malgn-noti-api\u002Fdoc\u002FDATA-MODEL.md)",{"id":300,"depth":576,"text":4410},"2. ERD (malgn-noti-api\u002Fdoc\u002FERD.md)",{"id":341,"depth":576,"text":4412},"3. 확장성·파티셔닝 전략 (malgn-noti-api\u002Fdoc\u002FSCALABILITY.md)",{"id":467,"depth":576,"text":4414},"4. 초기 DDL (malgn-noti-api\u002Fsrc\u002Fdb\u002Fmigrations\u002F0000_initial.sql)",{"id":536,"depth":576,"text":4416},"5. Hyperdrive 연결 (malgn-noti-api\u002Fwrangler.toml + 신규 코드)",{"id":651,"depth":576,"text":652},{"id":699,"depth":576,"text":700},{"id":748,"depth":576,"text":749,"children":4420},[4421,4423,4424],{"id":753,"depth":582,"text":4422},"신규 파일 (malgn-noti-api\u002F)",{"id":800,"depth":582,"text":801},{"id":832,"depth":582,"text":833},{"id":862,"depth":576,"text":4426},"10. 운영 컨벤션 명문화 (malgn-noti-api\u002FCLAUDE.md §8.1)",{"id":959,"depth":576,"text":960},{"id":1062,"depth":576,"text":1063,"children":4429},[4430,4432,4433,4434,4435,4437,4439,4441,4443],{"id":1073,"depth":582,"text":4431},"12.1 \u002Fadmin\u002F* 라우트 (malgn-noti-api\u002Fsrc\u002Froutes\u002Fadmin.ts)",{"id":1172,"depth":582,"text":1173},{"id":1340,"depth":582,"text":1341},{"id":1385,"depth":582,"text":1386},{"id":1416,"depth":582,"text":4436},"12.5 결정 — admin 라우트는 로컬 전용 (선택 A 유지)",{"id":1431,"depth":582,"text":4438},"12.6 환경 가드 추가 — 실수로도 프로덕션에 안 뚫리도록 (63ba424)",{"id":1613,"depth":582,"text":4440},"12.7-pdf ERD를 인쇄용 PDF로 (malgn-noti-api\u002Fdoc\u002FERD.pdf, 470b55a)",{"id":1653,"depth":582,"text":4442},"12.7 마이그레이션 절차 정본 — malgn-noti-api\u002Fdoc\u002FMIGRATION.md (47afe1a)",{"id":1674,"depth":582,"text":1675},{"id":1710,"depth":576,"text":4445,"children":4446},"13. 기본 CRUD API 골격 — Hono + Drizzle + Zod (a146e81, 8128468, 2b6f720)",[4447,4449,4451,4453,4455],{"id":1730,"depth":582,"text":4448},"13.1 사전 픽스 (a146e81)",{"id":1767,"depth":582,"text":4450},"13.2 의존성 (8128468)",{"id":1790,"depth":582,"text":4452},"13.3 인프라 + 라우트 (2b6f720)",{"id":1940,"depth":582,"text":4454},"13.4 검증 (pnpm dev --remote + curl)",{"id":2098,"depth":582,"text":2099},{"id":2162,"depth":576,"text":4457,"children":4458},"14. API 문서 페이지 — \u002Fdoc Scalar UI (beef401)",[4459,4460,4462,4463],{"id":2173,"depth":582,"text":2174},{"id":2191,"depth":582,"text":4461},"14.2 내용 (src\u002Fopenapi.ts)",{"id":2314,"depth":582,"text":2315},{"id":2341,"depth":582,"text":2342},{"id":2359,"depth":576,"text":2360,"children":4465},[4466,4468,4469,4471],{"id":2370,"depth":582,"text":4467},"15.1 배포 흐름 (malgn-noti-api\u002FCLAUDE.md §8.1 준수)",{"id":2383,"depth":582,"text":2384},{"id":2403,"depth":582,"text":4470},"15.3 검증 (https:\u002F\u002Fmalgn-noti-api.malgnsoft.workers.dev)",{"id":2515,"depth":582,"text":2516},{"id":2529,"depth":576,"text":4473,"children":4474},"16. 14개 도메인 라우트 추가 — 발신정보 \u002F 주소록 \u002F 템플릿 \u002F 문의 \u002F 결제 (3bd9864)",[4475,4477,4478,4479,4480],{"id":2539,"depth":582,"text":4476},"16.1 스키마 확장 (src\u002Fdb\u002Fschema.ts)",{"id":2611,"depth":582,"text":2612},{"id":2805,"depth":582,"text":2806},{"id":2838,"depth":582,"text":2839},{"id":2874,"depth":582,"text":2875},{"id":2914,"depth":576,"text":4482,"children":4483},"17. Phase 1·2·3 — \u002Fdoc 동기화 + 인증 + 발송 이력 (32f7ce4, c19f116, f45ad01)",[4484,4486,4488],{"id":2930,"depth":582,"text":4485},"17.1 Phase 1 — openapi.ts 확장 (32f7ce4)",{"id":2988,"depth":582,"text":4487},"17.2 Phase 2 — 인증 (signup \u002F login + JWT) (c19f116)",{"id":3107,"depth":582,"text":4489},"17.3 Phase 3 — 발송 이력 read-only (f45ad01)",{"id":3223,"depth":576,"text":3224,"children":4491},[4492,4493,4494,4495,4496],{"id":3230,"depth":582,"text":3231},{"id":3251,"depth":582,"text":3252},{"id":3274,"depth":582,"text":3275},{"id":3401,"depth":582,"text":3402},{"id":3412,"depth":582,"text":3413},{"id":3427,"depth":576,"text":4498,"children":4499},"19. POST \u002Fsend\u002Fsms — 발송 producer (DB 적재까지) (fb99b66)",[4500,4501,4502,4503,4504],{"id":3434,"depth":582,"text":3435},{"id":3475,"depth":582,"text":3476},{"id":3501,"depth":582,"text":3502},{"id":3542,"depth":582,"text":3543},{"id":3581,"depth":582,"text":3582},{"id":3648,"depth":576,"text":3649,"children":4506},[4507,4508,4510,4511,4512],{"id":3658,"depth":582,"text":3659},{"id":3675,"depth":582,"text":4509},"20.2 검증 (https:\u002F\u002Fmalgn-noti-api.malgnsoft.workers.dev)",{"id":3800,"depth":582,"text":3801},{"id":3815,"depth":582,"text":3816},{"id":3825,"depth":582,"text":3826},{"id":3846,"depth":576,"text":3847},{"id":3895,"depth":576,"text":4515,"children":4516},"22. 🐛 멱등 버그 해결 — TB_IDEMPOTENCY + INSERT-then-conflict (020307f)",[4517,4518,4519,4520],{"id":3905,"depth":582,"text":3906},{"id":3981,"depth":582,"text":3982},{"id":4018,"depth":582,"text":4019},{"id":4055,"depth":582,"text":4056},{"id":4071,"depth":576,"text":4072,"children":4522},[4523,4524,4525,4526],{"id":4084,"depth":582,"text":4085},{"id":4127,"depth":582,"text":4128},{"id":4203,"depth":582,"text":4204},{"id":4303,"depth":582,"text":4304},{"id":4314,"depth":576,"text":4315},"md",{},true,"\u002Fhistory\u002Fhistory.20260526",{"title":5,"description":563},"history\u002Fhistory.20260526","RIk-WTDanK0NymPPuHC1C6TWMJIOkuhOG52nCRUXtIY",1780640234426]