Files
jaengseung-made/scripts/test-flow.mjs
gahusb 2c9af41631 feat: 프로젝트 API Bearer 토큰 인증 + E2E 테스트 스크립트 + 크몽 마케팅 이미지
- app/api/projects, link/route: Cookie + Bearer 토큰 이중 인증 지원 (E2E 테스트 대응)
- app/mypage: 로또 기록 탭 제거, 구독 빈 상태 프롬프트 서비스로 변경
- scripts/test-flow.mjs: 견적서 발송→연결→마일스톤 진행 E2E 테스트 스크립트
- supabase/migrations/003: quotes RLS 비활성화 (관리자 서버 전용 접근)
- marketing/kmong-images: 크몽 서비스 A 상세 이미지 5장 (HTML 스크린샷용)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 04:15:47 +09:00

406 lines
15 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 프로젝트 추적 시스템 E2E 테스트 스크립트
* 실행: node scripts/test-flow.mjs
*
* 테스트 흐름:
* 1. 테스트 계정 생성 (Supabase Auth)
* 2. 관리자 로그인
* 3. 관리자 → 테스트 견적서 생성 + 발송
* 4. 테스트 유저 → 견적서 코드로 마이페이지 연결
* 5. 관리자 → 기본 7단계 마일스톤 초기화
* 6. 관리자 → 단계 진행 (1~3단계 완료)
* 7. 테스트 유저 → 마이페이지에서 진행 상황 확인 (브라우저)
*/
import { createClient } from '@supabase/supabase-js';
const BASE_URL = 'http://localhost:3000';
const SUPABASE_URL = 'https://avickbbhyhlnqbbqfzws.supabase.co';
const SUPABASE_ANON_KEY =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImF2aWNrYmJoeWhsbnFiYnFmendzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA4MjY0OTQsImV4cCI6MjA4NjQwMjQ5NH0.CFJCWlZpVzv3FQohhVuQLS8GbkvJrc7T7zDwltWuVZw';
const ADMIN_ID = 'jaengseung_admin';
const ADMIN_PASSWORD = 'JaengSeung@Admin2026!';
// 테스트 계정 정보
const TEST_EMAIL = `testuser_${Date.now()}@test-jaengseung.com`;
const TEST_PASSWORD = 'Test@2026!';
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
// ───────────────────────────────────────────
// 유틸
// ───────────────────────────────────────────
let _adminCookie = '';
function log(step, msg, data) {
const prefix = `\n[STEP ${step}]`;
console.log(`${prefix} ${msg}`);
if (data) console.log(' →', JSON.stringify(data, null, 2).slice(0, 400));
}
function ok(label, value) {
console.log(`${label}: ${value}`);
}
function fail(label, err) {
console.error(`${label}: ${err}`);
process.exit(1);
}
async function adminFetch(path, options = {}) {
const res = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
Cookie: _adminCookie,
...(options.headers ?? {}),
},
});
const text = await res.text();
let json;
try { json = JSON.parse(text); } catch { json = { raw: text }; }
return { status: res.status, data: json, headers: res.headers };
}
async function userFetch(path, accessToken, options = {}) {
const res = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
Cookie: `sb-avickbbhyhlnqbbqfzws-auth-token=${accessToken}`,
...(options.headers ?? {}),
},
});
const text = await res.text();
let json;
try { json = JSON.parse(text); } catch { json = { raw: text }; }
return { status: res.status, data: json };
}
// ───────────────────────────────────────────
// STEP 1: 테스트 유저 생성
// ───────────────────────────────────────────
async function step1_createUser() {
log(1, '테스트 계정 생성');
console.log(` 이메일: ${TEST_EMAIL}`);
console.log(` 비밀번호: ${TEST_PASSWORD}`);
const { data, error } = await supabase.auth.signUp({
email: TEST_EMAIL,
password: TEST_PASSWORD,
options: { data: { name: '테스트 고객' } },
});
if (error) fail('회원가입', error.message);
const user = data?.user;
if (!user) fail('회원가입', '유저 데이터 없음');
ok('유저 ID', user.id);
ok('이메일 확인 필요 여부', user.email_confirmed_at ? '이미 확인됨' : '미확인 (로컬 테스트는 통과)');
// 로그인으로 세션 획득
const { data: loginData, error: loginErr } = await supabase.auth.signInWithPassword({
email: TEST_EMAIL,
password: TEST_PASSWORD,
});
if (loginErr) fail('유저 로그인', loginErr.message);
const session = loginData?.session;
if (!session) fail('유저 로그인', '세션 없음 (이메일 인증 필요)');
ok('Access Token 획득', session.access_token.slice(0, 30) + '...');
return { userId: user.id, session };
}
// ───────────────────────────────────────────
// STEP 2: 관리자 로그인
// ───────────────────────────────────────────
async function step2_adminLogin() {
log(2, '관리자 로그인');
const res = await fetch(`${BASE_URL}/api/admin/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: ADMIN_ID, password: ADMIN_PASSWORD }),
});
if (res.status !== 200) fail('관리자 로그인', `HTTP ${res.status}`);
const setCookie = res.headers.get('set-cookie');
if (!setCookie) fail('관리자 로그인', 'Set-Cookie 헤더 없음');
_adminCookie = setCookie.split(';')[0]; // admin_token=xxx
ok('Admin Cookie', _adminCookie.slice(0, 50) + '...');
}
// ───────────────────────────────────────────
// STEP 3: 견적서 생성
// ───────────────────────────────────────────
async function step3_createQuote() {
log(3, '견적서 생성');
const { status, data } = await adminFetch('/api/admin/quotes', {
method: 'POST',
body: JSON.stringify({
title: '테스트 홈페이지 제작 견적서',
client_name: '테스트 고객',
client_email: TEST_EMAIL,
client_phone: '010-1234-5678',
valid_until: new Date(Date.now() + 30 * 86400000).toISOString().slice(0, 10),
items: [
{
id: 'item1',
name: '기업 홈페이지 제작',
description: 'Next.js 기반 반응형 홈페이지',
unitPrice: 1500000,
quantity: 1,
},
{
id: 'item2',
name: 'SEO 최적화',
description: '메타태그 + 사이트맵 설정',
unitPrice: 300000,
quantity: 1,
},
],
notes: '테스트 플로우용 견적서입니다. 납기 2주 예정.',
}),
});
if (status !== 201) fail('견적서 생성', `HTTP ${status} - ${JSON.stringify(data)}`);
const quote = data.quote;
ok('견적서 ID', quote.id);
ok('public_token', quote.public_token);
ok('상태', quote.status);
return quote;
}
// ───────────────────────────────────────────
// STEP 4: 견적서 상태를 'sent'로 변경
// ───────────────────────────────────────────
async function step4_sendQuote(quoteId) {
log(4, "견적서 상태 → 'sent' (발송)");
const { status, data } = await adminFetch(`/api/admin/quotes/${quoteId}`, {
method: 'PUT',
body: JSON.stringify({ status: 'sent' }),
});
if (status !== 200) fail('견적서 발송', `HTTP ${status} - ${JSON.stringify(data)}`);
ok('새 상태', data.quote?.status);
return data.quote;
}
// ───────────────────────────────────────────
// STEP 5: 테스트 유저가 견적서 코드 연결
// ───────────────────────────────────────────
async function step5_linkQuote(publicToken, session) {
log(5, '테스트 유저 → 견적서 코드 연결 (마이페이지)');
// 유저 세션 쿠키로 API 호출
const res = await fetch(`${BASE_URL}/api/projects/link`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${session.access_token}`,
},
body: JSON.stringify({ token: publicToken }),
});
const data = await res.json();
console.log(` HTTP ${res.status}:`, JSON.stringify(data));
if (res.status === 200 && data.success) {
ok('연결 성공', `quoteId=${data.quoteId}`);
return true;
} else {
// RLS 제한으로 실패할 수 있음 — 안내 메시지 출력
console.warn('\n ⚠️ 연결 API가 실패했습니다.');
console.warn(' 원인: SUPABASE_SERVICE_ROLE_KEY 가 비어 있어 RLS 우회 불가');
console.warn(' 해결: 아래 STEP 5b 안내 따라 서비스 롤 키를 추가하거나,');
console.warn(' 브라우저에서 직접 마이페이지 > "견적서 코드 입력" 테스트하세요.');
return false;
}
}
// ───────────────────────────────────────────
// STEP 6: 기본 7단계 마일스톤 초기화
// ───────────────────────────────────────────
async function step6_initMilestones(quoteId) {
log(6, '기본 7단계 마일스톤 초기화');
const { status, data } = await adminFetch('/api/admin/milestones', {
method: 'POST',
body: JSON.stringify({ useDefaults: true, quoteId }),
});
if (status !== 200 && status !== 201) fail('마일스톤 초기화', `HTTP ${status} - ${JSON.stringify(data)}`);
ok('마일스톤 생성 수', data.milestones?.length ?? 0);
data.milestones?.forEach((m) => console.log(` ${m.step_number}. [${m.status}] ${m.title}`));
return data.milestones;
}
// ───────────────────────────────────────────
// STEP 7: 마일스톤 단계 업데이트
// ───────────────────────────────────────────
async function step7_updateMilestones(milestones) {
log(7, '단계 진행 시뮬레이션 (1·2·3단계 완료, 4단계 진행중)');
const updates = [
{ id: milestones[0].id, status: 'completed', note: '고객 의뢰 접수 완료. 2주 일정 확인.' },
{ id: milestones[1].id, status: 'completed', note: '계약서 서명 및 선금 50% 입금 완료.' },
{ id: milestones[2].id, status: 'completed', note: '와이어프레임 v1 전달. 고객 승인.' },
{ id: milestones[3].id, status: 'in_progress', note: '디자인 시안 작업 중 (예상 3일).' },
];
for (const u of updates) {
const { status, data } = await adminFetch(`/api/admin/milestones/${u.id}`, {
method: 'PATCH',
body: JSON.stringify({ status: u.status, note: u.note }),
});
const m = milestones.find((x) => x.id === u.id);
if (status === 200) {
ok(`단계 ${m?.step_number} (${m?.title})`, `${u.status}`);
} else {
console.warn(` ⚠️ 단계 업데이트 실패: ${JSON.stringify(data)}`);
}
}
}
// ───────────────────────────────────────────
// STEP 8: 유저 마이페이지 API 검증
// ───────────────────────────────────────────
async function step8_verifyUserView(session) {
log(8, '유저 마이페이지 API 확인');
const res = await fetch(`${BASE_URL}/api/projects`, {
headers: {
Authorization: `Bearer ${session.access_token}`,
},
});
const data = await res.json();
console.log(` HTTP ${res.status}:`, JSON.stringify(data).slice(0, 500));
if (res.status === 200 && data.projects?.length > 0) {
const p = data.projects[0];
ok('프로젝트 제목', p.title);
ok('상태', p.status);
ok('마일스톤 수', p.milestones?.length ?? 0);
const done = p.milestones?.filter((m) => m.status === 'completed').length ?? 0;
const inProg = p.milestones?.filter((m) => m.status === 'in_progress').length ?? 0;
ok('완료 단계', done);
ok('진행중 단계', inProg);
} else {
console.warn(' ⚠️ 프로젝트가 없거나 연결 실패. 브라우저 직접 확인 필요.');
}
}
// ───────────────────────────────────────────
// 최종 안내 출력
// ───────────────────────────────────────────
function printBrowserGuide(quote, email, password, linked) {
console.log('\n' + '═'.repeat(60));
console.log('📋 브라우저 확인 가이드');
console.log('═'.repeat(60));
console.log(`
[관리자 화면]
URL: http://localhost:3000/admin/login
ID : jaengseung_admin
PW : JaengSeung@Admin2026!
→ 로그인 후 견적서 목록에서 아래 견적서 클릭:
"${quote.title}" (${quote.id.slice(0, 8)}...)
→ "진행 단계" 탭에서 마일스톤 상태 확인
→ 단계 상태 변경 → 저장 테스트
[고객(테스트 유저) 화면]
URL : http://localhost:3000/login
이메일: ${email}
비밀번호: ${password}
→ 로그인 후 마이페이지 → "프로젝트 현황" 탭
`);
if (!linked) {
console.log(` ⚠️ 자동 연결이 안 된 경우:
→ 마이페이지 아래 "견적서 코드 입력" 박스에 아래 코드 입력:
${quote.public_token}
`);
} else {
console.log(` ✅ 견적서가 이미 연결됨. 바로 "프로젝트 현황" 탭 확인 가능.
`);
}
console.log(`[확인 포인트]
- 진행률 막대 (완료 3/7 → 약 43%)
- 현재 진행 단계 "디자인 시안" 강조 표시
- 완료 단계에 ✓ 체크 아이콘 + 날짜
- 메모 내용 표시
[추가 테스트]
1. 관리자에서 "4단계 완료", "5단계 진행중"으로 변경
2. 새로고침 없이 마이페이지 탭 재진입 → 즉시 반영 확인
3. 최종 납품 완료(7단계)로 변경 → 상태 배지 변경 확인
`);
console.log('═'.repeat(60));
}
// ───────────────────────────────────────────
// 메인
// ───────────────────────────────────────────
(async () => {
console.log('\n🚀 쟁승메이드 프로젝트 추적 시스템 E2E 테스트 시작\n');
try {
// 1. 테스트 유저 생성
const { userId, session } = await step1_createUser().catch(async () => {
// 이미 존재하면 로그인만
console.log(' 이미 존재하는 계정으로 로그인 시도...');
const { data, error } = await supabase.auth.signInWithPassword({
email: TEST_EMAIL,
password: TEST_PASSWORD,
});
if (error) fail('유저 로그인', error.message);
return { userId: data.user.id, session: data.session };
});
// 2. 관리자 로그인
await step2_adminLogin();
// 3. 견적서 생성
const quote = await step3_createQuote();
// 4. 견적서 발송
await step4_sendQuote(quote.id);
// 5. 유저가 견적서 연결
const linked = await step5_linkQuote(quote.public_token, session);
// 6. 마일스톤 초기화
const milestones = await step6_initMilestones(quote.id);
// 7. 단계 업데이트
if (milestones?.length >= 4) {
await step7_updateMilestones(milestones);
}
// 8. 유저 뷰 검증
await step8_verifyUserView(session);
// 최종 안내
printBrowserGuide(quote, TEST_EMAIL, TEST_PASSWORD, linked);
console.log('\n✅ 테스트 스크립트 완료!\n');
} catch (err) {
console.error('\n❌ 예상치 못한 오류:', err.message ?? err);
process.exit(1);
}
})();