fix: 자동배포 임시 deployer 컨테이너 분리 + 푸시 알림 denied 안내 강화
Deploy momo-erp / deploy (push) Failing after 12m55s
Deploy momo-erp / deploy (push) Failing after 12m55s
자동배포 (모든 워크플로 빨강 문제 근본 해결): - 옛 흐름: 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-6 space-y-3">
|
||||
<h3 className="font-bold text-slate-700 mb-3 border-b pb-2 inline-flex items-center gap-2">
|
||||
@@ -264,20 +270,48 @@ export function PushOptIn({ variant = "compact" }: PushOptInProps) {
|
||||
>
|
||||
<span className={`inline-block h-6 w-6 transform rounded-full bg-white shadow transition-transform ${on ? "translate-x-[22px]" : "translate-x-0.5"}`} />
|
||||
</button>
|
||||
{denied && (
|
||||
<span className="text-[11px] text-rose-500 inline-flex items-center gap-1">
|
||||
<AlertCircle size={12} /> 기기/브라우저 설정에서 알림 허용 필요
|
||||
</span>
|
||||
)}
|
||||
{busy && <span className="text-[11px] text-slate-400">처리 중…</span>}
|
||||
</div>
|
||||
{statusMsg && (
|
||||
|
||||
{/* denied 상태: 코드로 권한 재요청 불가 → OS/브라우저 설정에서 직접 풀어야 */}
|
||||
{denied && (
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-lg p-3 text-xs text-slate-700 space-y-1.5">
|
||||
<div className="font-bold text-rose-700 inline-flex items-center gap-1">
|
||||
<AlertCircle size={13} /> 알림 권한이 차단된 상태입니다 — 직접 풀어주세요
|
||||
</div>
|
||||
{isTWA || isAndroid ? (
|
||||
<ol className="list-decimal list-inside leading-relaxed space-y-0.5 text-slate-600 pl-1">
|
||||
<li>안드로이드 <b>설정</b> 앱을 엽니다</li>
|
||||
<li><b>앱 → 모모유통</b> (또는 브라우저 이름) → <b>알림</b></li>
|
||||
<li><b>알림 표시</b>를 켜고 이 페이지로 돌아와 토글을 다시 누르세요</li>
|
||||
<li>또는 모모유통 앱 정보 화면에서 <b>저장공간 → 데이터 삭제</b> 후 재로그인</li>
|
||||
</ol>
|
||||
) : isiOS ? (
|
||||
<ol className="list-decimal list-inside leading-relaxed space-y-0.5 text-slate-600 pl-1">
|
||||
<li>iOS <b>설정 → 알림</b></li>
|
||||
<li>모모유통 앱(또는 Safari) 선택 → <b>알림 허용</b> 켜기</li>
|
||||
<li>이 페이지로 돌아와 토글을 다시 누르세요</li>
|
||||
</ol>
|
||||
) : (
|
||||
<ol className="list-decimal list-inside leading-relaxed space-y-0.5 text-slate-600 pl-1">
|
||||
<li>주소창 좌측 <b>자물쇠 아이콘</b> 클릭</li>
|
||||
<li><b>사이트 설정 → 알림</b> 을 <b>허용</b>으로 변경</li>
|
||||
<li>페이지 새로고침 후 토글을 다시 누르세요</li>
|
||||
</ol>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{statusMsg && !denied && (
|
||||
<div className="text-[11px] text-rose-600 bg-rose-50 border border-rose-100 rounded p-2 leading-snug">
|
||||
마지막 시도 결과: {statusMsg}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[10px] text-slate-400 leading-snug pt-1">
|
||||
진단 정보 · 권한: {typeof Notification !== "undefined" ? Notification.permission : "?"} · 보안 컨텍스트: {httpsOK ? "OK" : "NO"} · SW: {"serviceWorker" in (typeof navigator !== "undefined" ? navigator : ({} as Navigator)) ? "지원" : "미지원"}
|
||||
진단 · 권한: <b className={denied ? "text-rose-500" : "text-slate-600"}>{typeof Notification !== "undefined" ? Notification.permission : "?"}</b>
|
||||
{" · "}보안: <b className={httpsOK ? "text-emerald-600" : "text-rose-500"}>{httpsOK ? "OK" : "NO"}</b>
|
||||
{" · "}SW: <b>{"serviceWorker" in (typeof navigator !== "undefined" ? navigator : ({} as Navigator)) ? "지원" : "미지원"}</b>
|
||||
{" · "}환경: <b>{isTWA ? "TWA앱" : isAndroid ? "안드로이드" : isiOS ? "iOS" : "데스크탑/기타"}</b>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user