From 46eba2996f31c8e0b0bc0419bb1c96abace83495 Mon Sep 17 00:00:00 2001 From: chpark Date: Sat, 30 May 2026 16:29:41 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=9E=90=EB=8F=99=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=20deployer=20=EC=BB=A8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20=EB=B6=84=EB=A6=AC=20+=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20denied=20=EC=95=88=EB=82=B4=20=EA=B0=95?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 자동배포 (모든 워크플로 빨강 문제 근본 해결): - 옛 흐름: webhook → momo-erp 안의 sh → docker compose up → momo-erp 자기 자신 down 시점에 sh 도 같이 죽어 swap 중단 → 새 컨테이너 'Created' 상태로 멈춤 (실제 운영에서 관측) - 새 흐름: webhook → host docker daemon 에 임시 deployer 컨테이너 spawn - docker:cli 이미지에 git + compose-plugin apk add - root 로 git fetch + reset + build-sha + compose up - 임시 컨테이너는 momo-erp 와 PID 무관 → swap 완전 분리 - --rm 으로 종료 후 자동 정리, 로그는 /tmp/momo-deploy.log 누적 푸시 알림 (권한 denied 케이스): - 코드로 권한 재요청 불가능 → OS/브라우저 설정에서 직접 풀어야 함 - 카드에 환경(TWA/Android/iOS/데스크탑) 자동 감지 후 단계별 unblock 방법 표시 - TWA 앱은 안드로이드 설정 → 앱 → 모모유통 → 알림 허용 - 데스크탑 브라우저는 주소창 자물쇠 → 사이트 설정 → 알림 허용 Co-Authored-By: Claude Opus 4.7 --- src/app/api/deploy/webhook/route.ts | 40 +++++++++++++++++++++--- src/components/push-optin.tsx | 48 ++++++++++++++++++++++++----- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/app/api/deploy/webhook/route.ts b/src/app/api/deploy/webhook/route.ts index d31fac7..7b2a88d 100644 --- a/src/app/api/deploy/webhook/route.ts +++ b/src/app/api/deploy/webhook/route.ts @@ -24,11 +24,41 @@ export async function POST(req: NextRequest) { await writeFile(logPath, `[${new Date().toISOString()}] deploy 시작\n`); } catch { /* ignore */ } - // 백그라운드 실행 — 응답은 즉시 반환. - // sh 인터프리터로 명시 호출(스크립트 자체에 exec 권한 없어도 동작). - const child = spawn("/bin/sh", ["-c", - `(sh ${DEPLOY_SCRIPT} 2>&1 || echo "[deploy.sh 실행 실패 — 호스트에 docker.sock + 스크립트 마운트 필요]") >> ${logPath}` - ], { detached: true, stdio: "ignore" }); + // 임시 deployer 컨테이너로 분리 실행. + // momo-erp 자기 자신을 down → recreate 하는 순간 자기 안의 sh 프로세스도 같이 죽어버려 + // 배포가 도중에 끊겼던 문제를 해결. docker daemon 이 띄운 별도 컨테이너는 + // momo-erp 의 PID/network 와 무관하게 살아남는다. + // + // 임시 컨테이너: + // - root(--user 0:0) 로 실행 → .git permission 문제 없음 + // - 호스트 source 디렉토리와 docker.sock 마운트 + // - 종료 후 자동 삭제(--rm), 로그는 /tmp/momo-deploy.log 에 누적 + const deployCmd = [ + "docker", "run", "-d", "--rm", + "--name", `momo-deployer-${Date.now()}`, + "-v", "/home/chpark/momo-erp/source:/src", + "-w", "/src", + "-v", "/var/run/docker.sock:/var/run/docker.sock", + "--user", "0:0", + "docker:cli", + "sh", "-c", + [ + `echo "[$(date)] deployer 시작" >> ${logPath}`, + `apk add --no-cache git docker-cli-compose >/dev/null 2>&1`, + `git config --global --add safe.directory '*'`, + `git fetch origin >> ${logPath} 2>&1`, + `git reset --hard origin/main >> ${logPath} 2>&1`, + `git rev-parse HEAD > public/build-sha.txt`, + `echo "[$(date)] 배포 SHA: $(cat public/build-sha.txt)" >> ${logPath}`, + `docker compose -f docker-compose.prod.yml build momo-erp >> ${logPath} 2>&1`, + `docker compose -f docker-compose.prod.yml up -d --force-recreate momo-erp >> ${logPath} 2>&1`, + `docker restart traefik >> ${logPath} 2>&1`, + `docker image prune -f >> ${logPath} 2>&1`, + `echo "[$(date)] ✔ 배포 완료" >> ${logPath}`, + ].join(" && "), + ]; + + const child = spawn(deployCmd[0], deployCmd.slice(1), { detached: true, stdio: "ignore" }); child.unref(); return NextResponse.json({ diff --git a/src/components/push-optin.tsx b/src/components/push-optin.tsx index 91d8653..9d99001 100644 --- a/src/components/push-optin.tsx +++ b/src/components/push-optin.tsx @@ -243,6 +243,12 @@ export function PushOptIn({ variant = "compact" }: PushOptInProps) { } // === card 변형 (회원정보 페이지) === + // denied 환경 감지 — TWA 앱은 안드로이드 OS 설정, 브라우저는 사이트 설정에서 풀어야 + const ua = typeof navigator !== "undefined" ? navigator.userAgent : ""; + const isAndroid = /Android/i.test(ua); + const isiOS = /iPhone|iPad|iPod/i.test(ua); + const isTWA = isAndroid && (/wv|; ?Trusted Web Activity/i.test(ua) || (typeof document !== "undefined" && (document.referrer ?? "").startsWith("android-app://"))); + return (

@@ -264,20 +270,48 @@ export function PushOptIn({ variant = "compact" }: PushOptInProps) { > - {denied && ( - - 기기/브라우저 설정에서 알림 허용 필요 - - )} {busy && 처리 중…}

- {statusMsg && ( + + {/* denied 상태: 코드로 권한 재요청 불가 → OS/브라우저 설정에서 직접 풀어야 */} + {denied && ( +
+
+ 알림 권한이 차단된 상태입니다 — 직접 풀어주세요 +
+ {isTWA || isAndroid ? ( +
    +
  1. 안드로이드 설정 앱을 엽니다
  2. +
  3. 앱 → 모모유통 (또는 브라우저 이름) → 알림
  4. +
  5. 알림 표시를 켜고 이 페이지로 돌아와 토글을 다시 누르세요
  6. +
  7. 또는 모모유통 앱 정보 화면에서 저장공간 → 데이터 삭제 후 재로그인
  8. +
+ ) : isiOS ? ( +
    +
  1. iOS 설정 → 알림
  2. +
  3. 모모유통 앱(또는 Safari) 선택 → 알림 허용 켜기
  4. +
  5. 이 페이지로 돌아와 토글을 다시 누르세요
  6. +
+ ) : ( +
    +
  1. 주소창 좌측 자물쇠 아이콘 클릭
  2. +
  3. 사이트 설정 → 알림허용으로 변경
  4. +
  5. 페이지 새로고침 후 토글을 다시 누르세요
  6. +
+ )} +
+ )} + + {statusMsg && !denied && (
마지막 시도 결과: {statusMsg}
)}
- 진단 정보 · 권한: {typeof Notification !== "undefined" ? Notification.permission : "?"} · 보안 컨텍스트: {httpsOK ? "OK" : "NO"} · SW: {"serviceWorker" in (typeof navigator !== "undefined" ? navigator : ({} as Navigator)) ? "지원" : "미지원"} + 진단 · 권한: {typeof Notification !== "undefined" ? Notification.permission : "?"} + {" · "}보안: {httpsOK ? "OK" : "NO"} + {" · "}SW: {"serviceWorker" in (typeof navigator !== "undefined" ? navigator : ({} as Navigator)) ? "지원" : "미지원"} + {" · "}환경: {isTWA ? "TWA앱" : isAndroid ? "안드로이드" : isiOS ? "iOS" : "데스크탑/기타"}
);