cf. firebase에 관하여...
https://wikidocs.net/blog/@inetsos/2175/
11. firebase가 뭐야?
🔥 Firebase란? Firebase는 Google에서 제공하는 백엔드 서비스 플랫폼(BaaS, Backend as a Service) 이다. 👉 즉, 백엔드 서버 없이도 강력한 웹·모바일 앱을 개발할 수 있도록 도와주는 서비스! 1. 2. 3. 4. 5. 6.
wikidocs.net
🚀 Firebase의 주요 기능
1️⃣ Authentication (인증)
✅ 이메일/비밀번호, Google, Facebook, Kakao 로그인 등을 쉽게 구현 가능
✅ JWT 기반 인증, Firebase 자체 보안 관리
✅ Vue 3 + Firebase Auth 예제
2️⃣ Firestore (실시간 데이터베이스)
✅ NoSQL 기반의 클라우드 데이터베이스
✅ 데이터가 자동 동기화되며, 서버 없이도 가능
✅ Vue 3 + Firestore 예제
3️⃣ Firebase Hosting (웹 배포)
✅ 손쉽게 Vue.js 웹앱 배포 가능
✅ HTTPS 자동 적용
✅ 배포 명령어 (Vite 기준)
firebase init hosting
firebase deploy
4️⃣ Cloud Storage (파일 저장)
✅ 이미지, 동영상, 문서 등 파일을 저장하는 서비스
✅ Vue 3 + Firebase Storage 예제
5️⃣ Firebase Functions (서버리스 백엔드)
✅ Node.js 기반 백엔드 로직을 작성 가능
✅ 서버를 따로 두지 않고도 API 만들기 가능
✅ Firebase Functions 예제 (Cloud Firestore 트리거)
🔥 Firebase를 사용하면 좋은 점?
✅ 백엔드 서버 없이도 강력한 웹·모바일 앱 개발 가능
✅ 인증, 데이터베이스, 파일 저장, 배포까지 원스톱
✅ Google Cloud 기반으로 확장성 뛰어남
✅ 무료 요금제 제공 (Spark Plan)
🚀 결론
📌 Firebase를 사용하면 Vue 3 등 프론트엔드 기술만으로도 강력한 웹앱을 만들 수 있다!
📌 서버 없이도 실시간 데이터 관리, 인증, 배포까지 가능하다!
FDE(Forward Deployed Engineer) : 고객사에 직접 파견되는 엔지니어, 현장 상주, 문제 파악, 코딩 구현
palantir에서 시작
최종 발표안~
초안

첫번째 프롬프트
whichMenu 기능 1-A 개발
프로젝트 개요
whichMenu는 사용자가 오프라인 식당의 메뉴판 사진을 공유하고, 메뉴판에서 추출한 메뉴 정보를 온라인으로 제공하는 웹 앱이다.
과거에 개발한 레거시 앱이 있지만 해당 코드는 사용하지 않는다. 이번 프로젝트는 Google AI Studio에서 처음부터 새롭게 개발한다.
전체 핵심 기능은 다음과 같다.
- 식당 메뉴판 게시 및 공유
- 메뉴 검색 및 AI 추천
현재는 기능 1만 개발하며, 기능 2는 구현하지 않는다.
기술 스택
- React
- TypeScript
- Vite
- Firebase Web SDK
- Firebase Authentication
- Cloud Firestore
- Firebase Storage
- Firebase AI Logic
- Gemini 멀티모달 이미지 분석
다음 기술은 사용하지 않는다.
- 별도의 Node.js 또는 Express 백엔드
- Spring Boot
- Firebase Admin SDK
- 별도의 OCR 서비스
- 클라이언트에 직접 노출되는 일반 Gemini API 키
메뉴판의 글자와 메뉴 구조는 별도 OCR이 아니라 Gemini가 메뉴판 이미지를 직접 분석하여 추출한다.
기능 1의 전체 개발 단계
기능 1은 다음 순서로 나누어 개발한다.
- 식당 정보 입력 및 메뉴판 이미지 선택
- Gemini 메뉴판 이미지 분석
- 분석 결과 확인 및 수정
- Firebase 게시
현재 요청에서는 1단계만 구현한다.
Gemini 호출과 Firebase 저장은 아직 구현하지 않는다.
현재 구현 범위
식당 및 메뉴판 등록 화면을 구현한다.
입력 항목은 다음과 같다.
- 식당명
- 주소
- 음식 카테고리
- 식당 설명
- 메뉴판 유형
- 메뉴판 적용 날짜
- 메뉴판 이미지
메뉴판 유형은 다음 두 가지이다.
type MenuBoardType = "STATIC" | "DAILY";
- STATIC: 상시 메뉴판
- DAILY: 특정 날짜에 적용되는 메뉴판
DAILY를 선택한 경우에만 날짜 입력란을 표시하고 날짜를 필수로 입력받는다.
음식 카테고리는 다음 값을 사용한다.
- 한식
- 중식
- 일식
- 양식
- 분식
- 카페·디저트
- 치킨
- 피자
- 아시아 음식
- 기타
이미지 선택 규칙
현재는 메뉴판 이미지 한 장만 선택할 수 있도록 구현한다.
지원 형식:
- JPEG
- PNG
- WebP
최대 파일 크기:
- 10MB
다음 기능을 제공한다.
- 이미지 파일 선택
- 이미지 미리보기
- 파일명 표시
- 파일 크기 표시
- 이미지 교체
- 이미지 제거
- 파일 형식 검증
- 파일 크기 검증
이미지 미리보기에 Object URL을 사용한다면 이미지 변경 또는 컴포넌트 해제 시 URL을 정리한다.
입력값 검증
다음 항목은 필수이다.
- 식당명
- 주소
- 음식 카테고리
- 메뉴판 유형
- 메뉴판 이미지
- 날짜별 메뉴판의 적용 날짜
공백만 입력된 식당명과 주소는 허용하지 않는다.
오류 메시지는 해당 입력란 근처에 표시한다.
사용자가 처음부터 오류 메시지를 보게 하지 말고, 입력란을 벗어나거나 다음 버튼을 눌렀을 때 검증한다.
화면 구성
모바일 우선 반응형 화면으로 구현한다.
등록 진행 단계를 다음과 같이 표시한다.
1. 식당 정보
2. Gemini 분석
3. 메뉴 확인
4. 게시 완료
현재는 첫 번째 단계만 실제로 구현한다.
화면에는 다음 요소가 필요하다.
- whichMenu 헤더
- 등록 단계 표시
- 식당 정보 입력 영역
- 메뉴판 정보 입력 영역
- 이미지 업로드 영역
- 취소 버튼
- 다음 버튼
입력값 검증이 통과하면 입력한 내용과 이미지 미리보기를 보여주는 확인 화면으로 이동한다.
아직 Gemini를 연결하지 않았으므로 분석이 완료되었다거나 게시되었다는 메시지를 표시하지 않는다.
코드 작성 기준
- React 함수형 컴포넌트를 사용한다.
- TypeScript의 any 사용을 최소화한다.
- 하나의 파일에 전체 기능을 작성하지 않는다.
- 컴포넌트, 타입, 상수, 파일 검증 로직을 적절히 분리한다.
- 불필요한 상태 관리 라이브러리는 추가하지 않는다.
- 현재 단계에서는 React의 useState와 커스텀 훅을 우선 사용한다.
- 기능 2와 관련된 코드를 미리 만들지 않는다.
- 가짜 Firebase 저장이나 가짜 Gemini 응답을 구현하지 않는다.
- 현재 기능과 관계없는 서버 파일을 생성하지 않는다.
권장 구조는 다음과 같다.
src/
├─ components/
│ └─ menu-registration/
├─ constants/
├─ utils/
├─ App.tsx
├─ firebase.ts
├─ main.tsx
├─ types.ts
└─ index.css
완료 조건
다음 조건을 모두 만족해야 한다.
- React + TypeScript + Vite 프로젝트가 정상 실행된다.
- 식당 정보를 입력할 수 있다.
- 메뉴판 유형에 따라 날짜 입력란이 제어된다.
- 메뉴판 이미지를 선택하고 미리 볼 수 있다.
- 잘못된 형식과 크기의 파일을 차단한다.
- 이미지 교체와 제거가 가능하다.
- 필수 입력값 검증이 동작한다.
- 모바일과 데스크톱에서 화면이 깨지지 않는다.
- 빌드 오류와 콘솔 오류가 없다.
- Gemini와 Firebase 저장 기능은 아직 구현하지 않는다.
구현이 끝나면 다음 내용만 간단히 보고하고 작업을 중단한다.
- 생성하거나 수정한 주요 파일
- 구현한 기능
- 아직 구현하지 않은 기능
- 다음 단계에서 연결할 기능
첫번째 결과 일부
whichMenu 기능 1-A (식당 정보 및 메뉴판 이미지 등록 화면) 개발이 완료되었습니다.
주요 파일 목록
- src/types.ts: 식당 정보, 메뉴판 유형(STATIC | DAILY), 음식 카테고리, 폼 오류 타입 정의
- src/constants/categories.ts: 10개 음식 카테고리 목록, 4단계 진행 단계 정의, 파일 관련 상수 (10MB 제한, 허용 확장자)
- src/utils/fileValidation.ts: 파일 크기 포맷팅 및 이미지 형식/용량 검증 로직
- src/utils/formValidation.ts: 필수 입력값 및 공백 단독 입력 방지 검증 로직
- src/hooks/useImageUpload.ts: 이미지 선택, Object URL 생성 및 메모리 해제(URL.revokeObjectURL), 이미지 교체 및 제거 커스텀 훅
- src/components/menu-registration/Header.tsx: whichMenu 헤더 컴포넌트
- src/components/menu-registration/StepProgress.tsx: 4단계 메뉴판 등록 진행 단계 표시 바
- src/components/menu-registration/RestaurantForm.tsx: 식당명, 주소, 음식 카테고리, 설명 입력 폼
- src/components/menu-registration/MenuBoardTypeSelector.tsx: 메뉴판 유형(상시/날짜별) 선택 카드 및 날짜 입력란 제어
- src/components/menu-registration/ImageUploader.tsx: 드래그 앤 드롭 지원 이미지 업로드, 파일 정보(파일명, 용량) 및 미리보기/확대/교체/제거 컴포넌트
- src/components/menu-registration/Step1Confirmation.tsx: 1단계 검증 통과 시 입력 내용 검토 및 확인 화면
- src/App.tsx: 전체 폼 상태 관리, 검증 트리거, 뷰 전환 연결
구현한 기능
- 식당 및 메뉴판 정보 입력: 식당명, 주소, 음식 카테고리(한식, 중식, 일식 등 10종), 식당 설명(선택) 입력
- 메뉴판 유형 및 날짜 제어: 상시 메뉴판(STATIC)과 날짜별 메뉴판(DAILY)을 선택할 수 있으며, DAILY 선택 시에만 적용 날짜 입력란이 활성화되고 필수값으로 검증
- 이미지 업로드 및 미리보기: 이미지 파일 선택 및 드래그 앤 드롭, Object URL을 활용한 미리보기, 이미지 원본 확대 모달
- 파일 검증: JPEG, PNG, WebP 형식 검증 및 10MB 용량 초과 차단, 파일명 및 가독성 높은 용량(예: 2.45 MB) 표시
- 이미지 변경/제거: 이미지 교체 및 제거 기능 제공 (메모리 누수 방지를 위한 Object URL 자동 해제)
- 입력값 검증 및 에러 핸들링: 식당명/주소 공백 단독 입력 차단, 포커스 아웃(onBlur) 및 [다음] 버튼 클릭 시 해당 위치 근처에 검증 오류 메시지 표시
- 1단계 확인 화면: 검증을 통과하면 입력된 내용 및 메뉴판 이미지 미리보기를 요약하여 보여주고, [수정하기] 기능을 통해 양식으로 돌아올 수 있도록 구현
- 반응형 디자인: 모바일 및 데스크톱 기기에 맞춘 레이아웃 구성
아직 구현하지 않은 기능
- Gemini 멀티모달 이미지 분석 API 호출 및 텍스트/메뉴 자동 추출 (2단계)
- 추출된 메뉴 및 가격 정보 확인 및 수정 화면 (3단계)
- Firebase 저장 및 최종 게시 완료 처리 (4단계)
- 메뉴 검색 및 AI 추천 기능 (기능 2)
다음 단계에서 연결할 기능
- 기능 1-B (Gemini 멀티모달 이미지 분석): 등록한 메뉴판 이미지에서 Gemini AI를 활용해 메뉴명, 가격, 설명 텍스트를 자동 인식하는 로직 구현

두번째 프롬프트
whichMenu 기능 1-B: Gemini 메뉴판 분석
기존 기능 1-A의 코드와 UI를 유지하면서 기능 1-B만 구현해 주세요.
사용자가 선택한 메뉴판 이미지의 File 객체를 Firebase AI Logic Web SDK를 통해 Gemini 멀티모달 모델에 전달하고, 메뉴 정보를 구조화된 JSON으로 반환받아 주세요.
별도의 OCR 서비스, Node.js·Express 백엔드, Firebase Admin SDK는 사용하지 마세요.
기존 코드 연동
- 기존 firebase.ts의 Firebase 초기화를 재사용합니다.
- Firebase 앱을 중복 초기화하지 않습니다.
- 기존 useImageUpload에서 관리하는 원본 File 객체를 사용합니다.
- 기능 1-A의 입력값, 이미지 미리보기, 검증 기능을 깨뜨리지 않습니다.
- 1단계 확인 화면에서 사용자가 Gemini 분석 시작 버튼을 눌렀을 때 분석을 실행합니다.
Gemini 응답 구조
interface MenuAnalysisResult {
restaurantName: string | null;
menuItems: {
name: string;
price: number | null;
description: string | null;
category: string | null;
displayOrder: number;
}[];
}
단순히 JSON 형식으로 답하도록 프롬프트만 작성하지 말고, Gemini 모델 설정에 다음을 적용합니다.
- responseMimeType: "application/json"
- 위 타입과 일치하는 responseSchema
- 반환값 JSON 파싱
- 파싱 결과에 대한 런타임 구조 검증
Firebase AI Logic에서 현재 지원되는 안정적인 이미지 입력 및 구조화 출력 가능 모델을 사용하고, 모델 이름은 상수로 분리해 주세요.
이미지 분석 규칙
Gemini에 다음 규칙을 명확하게 전달합니다.
- 이미지에서 실제로 확인되는 메뉴만 추출
- 메뉴판 제목, 전화번호, 주소, 안내 문구를 메뉴로 처리하지 않음
- 확인할 수 없는 가격·설명·카테고리는 null
- 가격의 쉼표와 통화 기호를 제거하여 숫자로 반환
- 메뉴판에 표시된 순서를 유지
- displayOrder는 1부터 순서대로 부여
- 동일한 메뉴를 임의로 중복 생성하지 않음
- 보이지 않는 메뉴나 정보를 추측하여 생성하지 않음
- 메뉴를 발견하지 못하면 빈 menuItems 배열 반환
화면 상태
다음 분석 상태를 구현합니다.
type AnalysisStatus =
| "IDLE"
| "ANALYZING"
| "SUCCESS"
| "FAILED";
화면에는 다음 상태를 표시합니다.
- 분석 전
- 분석 중
- 분석 성공
- 분석 실패
- 메뉴를 찾지 못한 상태
분석 중에는 중복 요청을 막기 위해 분석 버튼을 비활성화합니다.
분석 실패 시 다음 선택지만 제공합니다.
- 다시 분석
- 직접 입력으로 진행
- 이전 화면으로 돌아가기
이번 단계에서는 직접 메뉴를 입력하거나 분석 결과를 편집하는 UI는 구현하지 않습니다. 해당 기능은 기능 1-C에서 구현합니다.
분석에 실패하더라도 기존 식당 정보와 선택한 이미지가 유지되어야 합니다.
분석 결과 표시
분석 성공 시 다음 내용을 읽기 전용으로 표시합니다.
- Gemini가 인식한 식당명
- 메뉴명
- 가격
- 설명
- 카테고리
- 메뉴 순서
아직 수정 기능은 제공하지 않습니다.
Gemini 결과를 Firestore나 Storage에 저장하지 않습니다.
가짜 분석 결과, 하드코딩된 메뉴, 임의의 성공 응답을 사용하지 않습니다.
이번 단계의 제외 범위
다음 기능은 구현하지 않습니다.
- 메뉴 수정·추가·삭제·순서 변경
- Firebase Authentication
- Firestore 저장
- Firebase Storage 업로드
- 게시 완료 처리
- 메뉴 검색
- 메뉴 추천
완료 조건
- 실제 선택 이미지가 Gemini에 전달됨
- 지정한 응답 스키마에 맞게 결과가 반환됨
- 응답 파싱과 구조 검증이 동작함
- 분석 상태와 오류 처리가 동작함
- 기존 기능 1-A가 정상 유지됨
- 빌드 오류와 콘솔 오류가 없음
- Firestore와 Storage에는 아무 데이터도 저장하지 않음
완료 후 다음 내용만 간단히 보고하고 작업을 중단해 주세요.
- 생성하거나 수정한 파일
- Gemini 호출 흐름
- 분석 결과 및 오류 처리 방식
- 아직 구현하지 않은 기능
두번째 결과
1. 생성하거나 수정한 파일
- src/firebase.ts: Firebase App 초기화 중복 방지 및 Firebase AI Logic Web SDK(firebase/ai) getAI 초기화 추가.
- src/types.ts: AnalysisStatus, MenuItem, MenuAnalysisResult 타입 정의.
- src/constants/gemini.ts: Gemini 모델 상수(GEMINI_MODEL_NAME = 'gemini-2.5-flash') 및 멀티모달 분석 지침 프롬프트 작성.
- src/services/geminiService.ts: File 객체 base64 변환, SchemaType 기반 JSON 스키마 정의, Gemini 멀티모달 호출, JSON 파싱 및 런타임 구조 검증 함수 작성.
- src/components/menu-registration/Step1Confirmation.tsx: 1단계 확인 화면 내 Gemini 분석 상태별(IDLE, ANALYZING, SUCCESS, FAILED) UI 표현, 읽기 전용 분석 결과 테이블, 실패 시 3가지 선택지 버튼 구현.
2. Gemini 호출 흐름
- 이미지 변환: 사용자가 선택한 원본 File 객체(formData.imageFile)를 FileReader를 이용해 base64 inlineData 파트로 변환합니다.
- SDK 호출: firebase/ai 패키지의 getGenerativeModel을 사용하여 responseMimeType: "application/json"과 responseSchema(MENU_ANALYSIS_SCHEMA)가 적용된 모델 인스턴스를 생성합니다.
- 분석 요청: 세부 추출 규칙이 포함된 프롬프트와 이미지 파트를 model.generateContent로 전달하여 분석을 수행합니다.
- 결과 수신: result.response.text()로 구조화된 JSON 문자열을 수신합니다.
3. 분석 결과 및 오류 처리 방식
- 구조 및 런타임 검증: 수신한 JSON을 파싱 후 validateAndSanitizeMenuAnalysis 함수를 통해 restaurantName 형식, menuItems 배열 유효성, 가격 숫자 정제(쉼표 및 통화기호 제거), displayOrder 1부터 순차 재정렬을 수행합니다.
- 상태 관리:
- IDLE: 분석 시작 전 안내 및 [Gemini AI 분석 시작] 버튼 활성화.
- ANALYZING: 분석 진행 중 로딩 애니메이션 표시 및 중복 요청 방지를 위한 버튼 비활성화.
- SUCCESS: 인식된 식당명과 메뉴 목록(메뉴명, 가격, 카테고리, 설명, 순서)을 읽기 전용으로 표시. 메뉴를 발견하지 못한 경우 안내 메시지 및 재시도/이전/직접입력 안내 제공.
- FAILED: 오류 메시지를 표시하고, 요구사항에 맞춰 1. 다시 분석, 2. 직접 입력으로 진행, 3. 이전 화면으로 돌아가기 3가지 선택지만 제공. 기존 입력 정보 및 선택 이미지는 유지됩니다.
4. 아직 구현하지 않은 기능
- 메뉴 항목 수정·추가·삭제·순서 변경 (기능 1-C 예정)
- Firebase Authentication 연동
- Firestore 데이터 저장
- Firebase Storage 이미지 업로드
- 게시 완료 처리, 메뉴 검색 및 추천 기능
세번째 프롬프트
whichMenu 기능 1-C: 메뉴 확인 및 수정
기존 기능 1-A와 1-B 코드를 유지하면서 기능 1-C만 구현해 주세요.
Gemini가 반환한 메뉴 분석 결과를 사용자가 확인하고 수정할 수 있는 편집 화면으로 변경합니다.
Firebase Authentication, Firestore 저장, Firebase Storage 업로드와 실제 게시 처리는 아직 구현하지 마세요.
편집 상태 관리
Gemini 분석 결과를 직접 변경하지 말고, 편집용 메뉴 배열을 별도로 생성해 관리해 주세요.
interface EditableMenuItem {
id: string;
name: string;
price: number | null;
description: string | null;
category: string | null;
displayOrder: number;
}
- 편집용 id는 React 렌더링과 항목 식별에 사용합니다.
- 새 메뉴를 추가할 때도 고유한 id를 생성합니다.
- Gemini가 반환한 원본 분석 결과는 가능하면 별도로 유지합니다.
- 기존 식당 정보와 선택한 이미지가 유지되어야 합니다.
사용자가 직접 입력한 식당명이 기준이며, Gemini가 분석한 restaurantName으로 기존 식당명을 자동 변경하지 마세요. Gemini 식당명은 참고 정보로만 표시합니다.
필요한 기능
다음 기능을 구현해 주세요.
- 메뉴명 수정
- 가격 수정
- 설명 수정
- 카테고리 수정
- 메뉴 직접 추가
- 메뉴 삭제
- 메뉴 순서 위로 이동
- 메뉴 순서 아래로 이동
- 모든 변경 사항 되돌리기
- Gemini 분석 다시 실행
- 수정 내용 최종 확인
이번 단계에서는 별도의 드래그 앤 드롭 라이브러리를 추가하지 말고, 위·아래 이동 버튼으로 순서를 변경해 주세요.
순서가 변경되거나 메뉴가 추가·삭제되면 displayOrder를 1부터 다시 계산합니다.
직접 입력 처리
Gemini 분석이 실패했거나 메뉴가 하나도 없는 경우 사용자가 직접 입력으로 진행을 선택하면 빈 편집 화면을 표시합니다.
빈 편집 화면에서 사용자가 메뉴를 직접 추가할 수 있어야 합니다.
직접 입력 기능은 이번 단계에 포함합니다.
입력값 검증
다음 검증을 적용해 주세요.
- 메뉴명은 필수
- 공백만 있는 메뉴명은 허용하지 않음
- 가격은 비어 있거나 0 이상의 숫자여야 함
- 가격 입력의 쉼표와 원화 기호는 제거하여 숫자로 변환
- 음수와 숫자가 아닌 값은 허용하지 않음
- 설명과 카테고리는 선택 사항
- 오류 메시지는 해당 메뉴 항목 가까이에 표시
- 오류가 있는 상태에서는 최종 확인 불가
가격 입력창에는 표시용 문자열과 저장용 숫자를 적절히 구분해 처리해 주세요.
메뉴가 없는 경우
사용자는 다음 중 하나를 선택할 수 있어야 합니다.
- 메뉴 직접 추가
- Gemini 다시 분석
- 이미지 전용 게시 방식으로 진행
- 이전 화면으로 돌아가기
이번 단계에서 이미지 전용 게시를 실제로 수행하지 않습니다.
이미지 전용 게시를 선택한 경우 다음 단계에서 사용할 수 있도록 상태만 설정합니다.
예:
type PublishMode = "WITH_MENU" | "IMAGE_ONLY";
- 메뉴를 확인한 경우: WITH_MENU
- 이미지만 게시하기로 한 경우: IMAGE_ONLY
Firebase에 실제로 저장하는 작업은 기능 1-D에서 구현합니다.
사용자 확인 절차
Gemini 분석 결과를 자동으로 확정하지 마세요.
사용자가 모든 메뉴를 확인한 후 메뉴 확인 완료 버튼을 눌러야 다음 단계로 이동할 수 있습니다.
확인 완료 시 다음 내용을 읽기 전용 요약 화면으로 보여 주세요.
- 식당명
- 메뉴판 유형
- 적용 날짜
- 메뉴판 이미지
- 최종 메뉴 수
- 수정이 완료된 메뉴 목록
- 게시 방식: 메뉴 포함 또는 이미지만 게시
아직 실제로 게시되었다는 성공 메시지는 표시하지 마세요.
분석 다시 실행
사용자가 Gemini 분석을 다시 실행하면 현재 수정 내용이 사라질 수 있다는 확인 메시지를 표시해 주세요.
사용자가 동의한 경우에만 기존 기능 1-B의 분석 로직을 다시 실행합니다.
재분석에 실패해도 기존 편집 내용은 유지해 주세요.
이번 단계의 제외 범위
다음 기능은 구현하지 않습니다.
- Firebase Authentication
- Firebase Storage 이미지 업로드
- Firestore 데이터 저장
- 실제 게시 완료 처리
- Firestore 및 Storage Security Rules
- 메뉴 검색
- AI 메뉴 추천
- 별도의 백엔드 서버
완료 조건
다음 조건을 만족해야 합니다.
- Gemini 분석 결과를 편집할 수 있음
- 직접 메뉴를 추가할 수 있음
- 메뉴 수정·삭제·순서 변경이 가능함
- displayOrder가 항상 1부터 순서대로 재계산됨
- 메뉴명과 가격 검증이 동작함
- 직접 입력 방식으로 진입 가능함
- 이미지 전용 게시 상태를 선택할 수 있음
- 사용자 확인 전에는 결과가 확정되지 않음
- 기존 식당 정보와 이미지가 유지됨
- Firebase에는 아무 데이터도 저장되지 않음
- 기존 기능 1-A와 1-B가 정상 동작함
- 빌드 오류와 콘솔 오류가 없음
완료 후 다음 내용만 간단히 보고하고 작업을 중단해 주세요.
- 생성하거나 수정한 파일
- 편집 상태 관리 방식
- 메뉴 검증 및 순서 변경 방식
- 직접 입력과 이미지 전용 게시 처리 방식
- 아직 구현하지 않은 기능
세번째 진행중
네번째 프롬프트
# whichMenu 기능 1-D: Firebase 게시
기존 기능 1-A, 1-B, 1-C의 코드와 동작을 유지하면서
기능 1-D인 Firebase 실제 게시 기능만 구현해 주세요.
이번 프로젝트에서는 로그인과 회원 기능을 사용하지 않습니다.
다음 기능은 구현하지 마세요.
- Firebase Authentication
- Google 로그인
- 익명 로그인
- Firebase App Check
- 별도의 Node.js 또는 Express 백엔드
- Firebase Admin SDK
- 기존 게시물 수정 및 삭제
- 메뉴 검색 및 AI 추천
## 1. 게시자 닉네임
기존 식당 정보 입력 화면에 다음 필드를 추가해 주세요.
publisherNickname: string
검증 규칙:
- 필수 입력
- 앞뒤 공백 제거
- 공백만 입력할 수 없음
- 2자 이상 20자 이하
- 줄바꿈 입력 불가
다음 화면에도 닉네임을 표시해 주세요.
- 식당 정보 확인 화면
- 최종 메뉴 확인 화면
- 게시 완료 화면
닉네임은 로그인 계정이나 게시물 소유권을 의미하지 않습니다.
## 2. Firebase 연결
기존 src/firebase.ts의 Firebase App과 Firebase AI Logic 설정을 유지해 주세요.
동일한 Firebase App 인스턴스에 다음 서비스만 추가해 주세요.
- Cloud Firestore
- Firebase Storage
Firebase App을 다른 파일에서 다시 initializeApp() 하지 마세요.
## 3. 게시 방식
기존 PublishMode를 사용해 주세요.
type PublishMode = "WITH_MENU" | "IMAGE_ONLY";
WITH_MENU:
- 메뉴판 이미지 저장
- 식당 정보 저장
- 메뉴판 정보 저장
- 사용자가 최종 확인한 메뉴 저장
IMAGE_ONLY:
- 메뉴판 이미지 저장
- 식당 정보 저장
- 메뉴판 정보 저장
- menuItems 문서는 생성하지 않음
Gemini 원본 결과가 아니라 기능 1-C에서 사용자가 최종 확인한 editableItems를 저장해 주세요.
WITH_MENU인데 유효한 메뉴가 없으면 게시를 차단해 주세요.
## 4. Firebase Storage
기존 원본 File 객체를 다음 경로에 업로드해 주세요.
menuBoards/{menuBoardId}/{uniqueFileName}
요구사항:
- uploadBytesResumable 사용
- 업로드 진행률 표시
- JPEG, PNG, WebP만 허용
- 최대 파일 크기 10MB
- 파일명 충돌 방지
- 업로드 완료 후 다운로드 URL 획득
- Storage 경로와 다운로드 URL을 Firestore에 저장
- 처리 중 중복 업로드 방지
## 5. Firestore 컬렉션
다음 컬렉션을 사용해 주세요.
restaurants/{restaurantId}
menuBoards/{menuBoardId}
menuItems/{menuItemId}
Restaurant 문서:
{
name: string;
address: string;
category: string;
description: string | null;
createdAt: serverTimestamp();
updatedAt: serverTimestamp();
}
MenuBoard 문서:
{
restaurantId: string;
boardType: "STATIC" | "DAILY";
menuDate: string | null;
originalImageUrls: string[];
storagePaths: string[];
publishMode: "WITH_MENU" | "IMAGE_ONLY";
textAvailable: boolean;
extractionStatus: "COMPLETED" | "IMAGE_ONLY";
publisherNickname: string;
publishedAt: serverTimestamp();
createdAt: serverTimestamp();
updatedAt: serverTimestamp();
}
MenuItem 문서:
{
restaurantId: string;
menuBoardId: string;
name: string;
price: number | null;
description: string | null;
category: string | null;
displayOrder: number;
createdAt: serverTimestamp();
updatedAt: serverTimestamp();
}
createdBy, userId, ownerId, UID 등의 사용자 식별 필드는 저장하지 마세요.
IMAGE_ONLY인 경우:
- textAvailable: false
- extractionStatus: "IMAGE_ONLY"
- menuItems 문서 생성 안 함
WITH_MENU인 경우:
- textAvailable: true
- extractionStatus: "COMPLETED"
- 최종 확인된 editableItems 저장
## 6. 저장 순서
다음 순서로 처리해 주세요.
1. 전체 입력값 최종 검증
2. restaurantId, menuBoardId, menuItemId 생성
3. 메뉴판 이미지 Storage 업로드
4. 다운로드 URL 획득
5. Firestore writeBatch 생성
6. Restaurant, MenuBoard, MenuItem 문서 저장
7. batch.commit() 성공 후 게시 완료 처리
Firestore 문서는 가능한 한 하나의 writeBatch로 저장해 주세요.
게시가 완료되기 전에는 성공 메시지를 표시하지 마세요.
Storage 업로드 후 Firestore 저장이 실패하면:
- 기존 입력값과 메뉴 데이터를 유지
- 업로드된 Storage 경로를 콘솔과 오류 정보에 기록
- 사용자가 다시 시도할 수 있도록 처리
이번 단계에서는 업로드된 파일 자동 삭제 기능을 구현하지 마세요.
## 7. 게시 상태
다음 상태를 구현해 주세요.
type PublishStatus =
| "IDLE"
| "VALIDATING"
| "UPLOADING"
| "SAVING"
| "SUCCESS"
| "FAILED";
요구사항:
- 업로드 진행률 표시
- 현재 처리 단계 표시
- 처리 중 게시 버튼 비활성화
- 중복 제출 방지
- 실패 시 입력 정보, 이미지, 수정 메뉴 유지
- 실패 후 다시 게시 가능
## 8. 게시 완료 화면
게시 성공 후 다음 정보를 표시해 주세요.
- 식당명
- 게시자 닉네임
- 게시일시
- 메뉴판 이미지
- 메뉴판 유형
- 게시 방식
- 등록 메뉴 수
- 새 메뉴판 등록 버튼
게시일시는 Firestore의 publishedAt Timestamp를 사용하고,
한국 형식으로 표시해 주세요.
예:
게시자: 쥐피티
게시일시: 2026. 08. 02. 12:55
게시 성공 후에만 기존 등록 상태를 초기화해 주세요.
## 9. 최소 Security Rules
로그인 없는 MVP를 위한 Firestore Rules와 Storage Rules를 작성해 주세요.
Firestore:
- restaurants, menuBoards, menuItems 공개 읽기 허용
- 인증 없이 새 문서 생성 허용
- 기존 문서 수정 및 삭제 금지
- 필수 필드와 주요 데이터 타입 검증
- 다른 컬렉션 접근 금지
Storage:
- menuBoards 경로의 이미지 공개 읽기 허용
- 인증 없이 새 이미지 업로드 허용
- JPEG, PNG, WebP만 허용
- 최대 파일 크기 10MB
- 기존 파일 덮어쓰기, 수정, 삭제 금지
- 다른 경로 접근 금지
allow read, write: if true 형태의 전체 허용 규칙은 사용하지 마세요.
Rules는 파일 작성만으로 적용되지 않으므로
Firebase Console 또는 Firebase CLI로 배포해야 한다는 안내를 포함해 주세요.
## 완료 조건
- 게시자 닉네임 입력과 검증이 동작함
- 로그인 없이 게시 가능함
- 이미지가 Firebase Storage에 업로드됨
- Restaurant와 MenuBoard 문서가 저장됨
- WITH_MENU에서 MenuItem 문서가 저장됨
- IMAGE_ONLY에서는 MenuItem 문서가 생성되지 않음
- publishedAt, createdAt, updatedAt에 serverTimestamp 사용
- 게시 상태와 업로드 진행률이 표시됨
- 중복 제출이 차단됨
- 실패 시 입력 데이터가 유지됨
- 기존 기능 1-A, 1-B, 1-C가 정상 유지됨
- 빌드 오류와 콘솔 오류가 없음
기능 1-D만 완료한 후 다음 내용만 간단히 보고하고 중단해 주세요.
1. 생성하거나 수정한 파일
2. 게시자 닉네임 처리 방식
3. Storage 및 Firestore 저장 흐름
4. 실패 및 중복 제출 처리 방식
5. Security Rules 정책
6. Firebase Console에서 직접 설정해야 할 내용
네번째 결과
주요 구현 내역
- 게시자 닉네임 (publisherNickname) 추가 및 검증
- 식당 정보 입력 폼: 2자 이상 20자 이하, 공백 및 줄바꿈 제한 검증 규칙 적용.
- 화면 노출: 1단계 식당 정보 확인 화면, 최종 메뉴 확인 화면 및 게시 완료 상세 명세 카드에 닉네임을 일관되게 표기.
- Firebase Storage 이미지 업로드 (uploadBytesResumable)
- 선택한 메뉴판 이미지 파일 객체를 menuBoards/{menuBoardId}/{uniqueFileName} 경로로 업로드.
- 업로드 진행률(0% ~ 100%)을 실시간으로 추적하여 사용자에게 프로그레스 바 및 상태 메시지 제공.
- Cloud Firestore 원자적 배치 저장 (writeBatch)
- restaurants/{restaurantId}: 식당 기본 정보 저장 (name, address, category, description).
- menuBoards/{menuBoardId}: 메뉴판 메타데이터 저장 (boardType, originalImageUrls, storagePaths, publishMode, textAvailable, extractionStatus, publisherNickname, publishedAt).
- menuItems/{menuItemId}: WITH_MENU 모드인 경우 확정된 메뉴 항목 개별 문서 생성 (restaurantId, menuBoardId, name, price, description, category, displayOrder).
- 모든 날짜 필드에 serverTimestamp() 적용.
- 게시 상태 및 예외 처리 UI
- 상태 관리: IDLE → VALIDATING → UPLOADING → SAVING → SUCCESS / FAILED.
- 실패 시 재시도: 실패 시에도 사용자가 입력한 식당 정보, 선택된 이미지 및 수정된 메뉴 배열을 100% 보존하며 [다시 게시하기] 버튼 제공.
- 게시 완료 화면 (PublishCompleteView): 게시 완료 후 Firestore / Storage에 등록된 상세 내역과 원본 이미지 크게 보기, 새 메뉴판 등록 기능 제공.
- 보안 규칙 구성 (firestore.rules & storage.rules)
- restaurants, menuBoards, menuItems 문서 및 Storage 경로에 대해 허용 조건별 읽기/쓰기 보안 정책 파일 작성.

오류 생김

해결법
현재 메뉴판 이미지 분석이 Firebase AI Logic 호출에서
API_KEY_INVALID 오류로 실패하고 있습니다.
Firebase AI Logic Web SDK를 통한 클라이언트 Gemini 호출을 제거하고,
Google AI Studio가 자동으로 제공하는 서버 측 Gemini API 호출 방식으로
변경해 주세요.
요구사항:
- 기존 메뉴판 분석 프롬프트와 JSON 응답 구조 유지
- 기존 메뉴 편집 및 Firebase 게시 기능 유지
- Google AI Studio의 서버 측 GEMINI_API_KEY Secret 사용
- Gemini API 키를 React 클라이언트에 노출하지 않음
- 메뉴판 File을 서버 Gemini 호출에 전달
- 별도의 Firebase AI Logic 초기화 제거
- Firestore와 Firebase Storage 코드는 유지
- 가짜 분석 결과를 사용하지 않음
- 기존 기능 1-A부터 1-D를 깨뜨리지 않음
수정 후 실제 메뉴판 이미지 분석이 동작하는지 확인하고,
수정한 파일과 호출 흐름만 보고해 주세요.
다만 이 경우 “직접 만든 별도 백엔드”는 아니지만, 기술적으로는 AI Studio가 제공하는 Node.js 서버 런타임을 사용하게 됩니다. AI Studio 웹 앱은 기본적으로 React 클라이언트와 Node.js 서버 런타임을 지원합니다.
오류수정 프롬프트 결과 및 AI 분석 상황 << 여기야!
Firebase AI Logic 클라이언트 SDK 호출에서 발생하던 API_KEY_INVALID 오류를 해결하고, Google AI Studio에서 제공하는 서버 측 Gemini API(process.env.GEMINI_API_KEY) 기반으로 메뉴판 이미지 분석 방식을 전환했습니다.
📁 수정한 주요 파일
- server.ts (신규 전용 서버)
- Express backend 엔드포인트 /api/analyze-menu 구축
- @google/genai (GoogleGenAI) SDK를 사용하여 서버 측에서 process.env.GEMINI_API_KEY 안전하게 참조
- 기존 메뉴판 분석 전용 시스템 프롬프트 및 Type.OBJECT 구조적 JSON Schema(gemini-2.5-flash) 유지
- package.json
- Express + Vite 풀스택 구동을 위해 "dev": "tsx server.ts", "build", "start" 스크립트 업데이트
- src/firebase.ts
- firebase/ai 모듈 및 getAI() 클라이언트 초기화 코드 제거
- Cloud Firestore(db) 및 Firebase Storage(storage) 기존 설정 완벽 유지
- src/services/geminiService.ts
- 클라이언트 analyzeMenuBoardImage() 함수에서 /api/analyze-menu로 이미지 Base64 데이터를 POST 전송하도록 변경
- Gemini 서버 분석 결과를 수신 후 기존 데이터 정형화 및 유효성 검증 함수(validateAndSanitizeMenuAnalysis) 연결
- src/components/menu-registration/Step1Confirmation.tsx
- UI 상의 분석 엔진 라벨을 서버 AI 방식(Gemini 2.5 Flash)으로 변경
🔄 수정된 데이터 호출 흐름
- [클라이언트] 이미지 로드: 사용자가 메뉴판 사진을 업로드하고 [분석 시작] 클릭
- [클라이언트 ➔ 서버 API]: File 객체를 Base64 변환 후 /api/analyze-menu POST 요청
- [서버] Gemini API 호출: server.ts에서 process.env.GEMINI_API_KEY로 @google/genai SDK 호출 후 gemini-2.5-flash 모델 분석 실행
- [서버 ➔ 클라이언트]: 추출된 식당명 및 메뉴 항목 JSON 반환
- [클라이언트 메뉴 편집 및 게시]: 기존 기능 1-C(메뉴 수동 편집) 및 기능 1-D(Firebase Firestore/Storage 최종 게시) 정상 수행


~중략~



두번째 오류,, storage bucket이 유료일 수 있다고 함,
메뉴판 이미지 선택 → Gemini 분석에만 사용 → 분석 후 이미지 폐기 → Firestore에는 메뉴 텍스트만 저장
이 방식으로 변경하겠음
기존: 메뉴판 사진과 추출 메뉴를 함께 공유 변경: 메뉴판 사진으로 추출한 메뉴 텍스트만 공유

사용한 프롬프트
# whichMenu 기능 1-D 수정: 메뉴판 이미지 비저장 및 텍스트 게시
기존 기능 1-A, 1-B, 1-C와 Gemini 메뉴판 분석 기능을 유지하면서
기능 1-D의 게시 방식을 수정해 주세요.
메뉴판 이미지는 Gemini 분석과 사용자 확인에만 사용하고,
Firebase Storage나 다른 이미지 저장소에는 저장하지 않습니다.
최종적으로 Cloud Firestore에는 식당, 메뉴판 메타데이터,
사용자가 확인한 메뉴 텍스트만 저장합니다.
## 1. 유지할 기능
다음 기존 기능은 유지해 주세요.
- 식당 정보 입력
- 게시자 닉네임 입력
- 메뉴판 이미지 선택 및 미리보기
- 이미지 형식과 크기 검증
- server.ts의 /api/analyze-menu 엔드포인트
- 서버 측 process.env.GEMINI_API_KEY 사용
- Gemini 2.5 Flash 메뉴판 이미지 분석
- 메뉴 분석 결과 확인 및 수정
- 메뉴 추가, 삭제, 순서 변경
- Gemini 분석 실패 시 직접 입력
- Firestore writeBatch 저장
- 게시 상태와 게시 완료 화면
## 2. 제거할 기능
다음 기능과 코드는 제거해 주세요.
- Firebase Storage 초기화
- getStorage()
- uploadBytesResumable()
- getDownloadURL()
- Firebase Storage 업로드 서비스
- storageBucket 설정 및 검증
- storage.rules
- originalImageUrls
- storagePaths
- imagePublicIds
- 이미지 업로드 진행률
- IMAGE_ONLY 게시 방식
- 이미지만 게시 버튼과 관련 상태
- 게시 완료 화면의 저장된 이미지 표시
Firebase Authentication, 로그인, Cloudinary도 추가하지 마세요.
## 3. 이미지 처리 방식
메뉴판 이미지는 다음 흐름으로만 사용해 주세요.
1. 사용자가 브라우저에서 이미지 File을 선택
2. Object URL을 이용해 로컬 미리보기 표시
3. 분석 시작 시 File을 Base64로 변환
4. Base64 이미지를 /api/analyze-menu로 전송
5. 서버가 Gemini에 이미지를 전달
6. Gemini 분석 결과 JSON만 클라이언트로 반환
7. 사용자가 원본 이미지와 분석 결과를 비교하고 수정
8. Firestore 게시 성공 후 File, Base64, Object URL을 정리
이미지와 Base64 데이터는 다음 위치에 저장하지 마세요.
- Cloud Firestore
- Firebase Storage
- 브라우저 localStorage
- 브라우저 sessionStorage
- IndexedDB
- 서버 파일 시스템
- 서버 로그
- 데이터베이스
- 장기 React 상태
Base64 문자열 전체와 이미지 요청 본문을 콘솔에 출력하지 마세요.
서버에서는 이미지를 임시 파일로 생성하지 말고 메모리에서 Gemini에 전달한 뒤 요청 처리가 끝나면 참조를 유지하지 마세요.
## 4. 이미지 유지 및 폐기 시점
Gemini 분석 직후에는 사용자가 결과를 원본과 비교할 수 있도록
선택한 File과 미리보기 Object URL을 브라우저 메모리에 유지해 주세요.
다음 상황에서 이미지 데이터를 정리해 주세요.
- Firestore 게시가 성공한 경우
- 사용자가 이미지를 제거한 경우
- 사용자가 다른 이미지로 교체한 경우
- 사용자가 새 메뉴판 등록을 시작한 경우
- 관련 컴포넌트가 해제되는 경우
Object URL은 반드시 URL.revokeObjectURL()로 해제해 주세요.
Firestore 저장에 실패한 경우에는 재시도를 위해
이미지 File, 미리보기, 식당 정보, 수정 메뉴를 유지해 주세요.
## 5. 이미지 전용 게시 제거
이미지를 저장하지 않으므로 IMAGE_ONLY 게시 기능은 사용할 수 없습니다.
기존 타입과 로직에서 다음 내용을 제거해 주세요.
type PublishMode = "WITH_MENU" | "IMAGE_ONLY";
PublishMode가 더 이상 필요하지 않으면 완전히 제거해 주세요.
다음 UI도 제거해 주세요.
- 이미지만 게시
- 이미지 전용 게시로 진행
- IMAGE_ONLY 안내
- 이미지 전용 게시 요약
Gemini 분석에 실패한 경우 사용자는 다음 중 하나만 선택할 수 있습니다.
- 다시 분석
- 메뉴 직접 입력
- 이전 화면으로 돌아가기
- 등록 취소
최소 한 개 이상의 유효한 메뉴가 있어야 Firestore에 게시할 수 있습니다.
## 6. Firestore 컬렉션
다음 컬렉션을 사용해 주세요.
restaurants/{restaurantId}
menuBoards/{menuBoardId}
menuItems/{menuItemId}
### Restaurant
{
name: string;
address: string;
category: string;
description: string | null;
createdAt: serverTimestamp();
updatedAt: serverTimestamp();
}
### MenuBoard
{
restaurantId: string;
boardType: "STATIC" | "DAILY";
menuDate: string | null;
publisherNickname: string;
analysisSource: "GEMINI" | "MANUAL";
menuCount: number;
publishedAt: serverTimestamp();
createdAt: serverTimestamp();
updatedAt: serverTimestamp();
}
### MenuItem
{
restaurantId: string;
menuBoardId: string;
name: string;
price: number | null;
description: string | null;
category: string | null;
displayOrder: number;
createdAt: serverTimestamp();
updatedAt: serverTimestamp();
}
다음 데이터는 Firestore에 저장하지 마세요.
- 이미지 URL
- 이미지 경로
- 이미지 파일명
- 이미지 Base64
- 원본 Gemini 응답 전체
- File 객체
- MIME 타입
- 이미지 크기
- 사용자 UID
analysisSource는 다음 규칙으로 저장해 주세요.
- Gemini 결과를 기반으로 편집한 경우: "GEMINI"
- Gemini 분석 없이 직접 메뉴를 입력한 경우: "MANUAL"
menuCount에는 최종 저장되는 MenuItem 개수를 저장해 주세요.
## 7. 게시 전 최종 검증
Firestore 저장 전에 다음 내용을 검증해 주세요.
- 식당명 필수
- 주소 필수
- 음식 카테고리 필수
- 게시자 닉네임 2자 이상 20자 이하
- 메뉴판 유형 필수
- DAILY인 경우 menuDate 필수
- 최종 메뉴가 한 개 이상 존재
- 모든 메뉴명 필수
- 메뉴명은 공백만 입력할 수 없음
- 가격은 null 또는 0 이상의 숫자
- displayOrder는 1부터 순차적으로 구성
이미지는 게시 대상이 아니므로 Firestore 게시 단계에서는
이미지 URL이나 Storage 상태를 검증하지 마세요.
## 8. Firestore 저장 순서
다음 순서로 처리해 주세요.
1. 중복 게시 요청 여부 확인
2. 전체 입력값 최종 검증
3. restaurantId 생성
4. menuBoardId 생성
5. 각 menuItemId 생성
6. Firestore writeBatch 생성
7. Restaurant 문서 등록
8. MenuBoard 문서 등록
9. 최종 확인된 MenuItem 문서 등록
10. batch.commit() 실행
11. 저장된 MenuBoard를 다시 조회하여 publishedAt 확인
12. 게시 완료 화면 표시
13. 게시 성공 후 이미지 File과 Object URL 정리
14. 새 메뉴판 등록을 선택하면 전체 상태 초기화
Gemini 원본 결과가 아니라 사용자가 최종 확인한 editableItems를 저장해 주세요.
## 9. 게시 상태
기존 게시 상태에서 UPLOADING을 제거해 주세요.
type PublishStatus =
| "IDLE"
| "VALIDATING"
| "SAVING"
| "SUCCESS"
| "FAILED";
다음 UI를 구현해 주세요.
- 게시 전 검증 중
- Firestore 저장 중
- 게시 성공
- 게시 실패
- 다시 게시
처리 중에는 게시 버튼을 비활성화하여 중복 저장을 차단해 주세요.
저장 실패 시 다음 데이터를 유지해 주세요.
- 식당 정보
- 게시자 닉네임
- 메뉴판 유형과 날짜
- 메뉴판 원본 File과 미리보기
- Gemini 분석 결과
- 사용자가 수정한 메뉴
## 10. 게시 완료 화면
게시 완료 화면에는 다음 정보만 표시해 주세요.
- 식당명
- 게시자 닉네임
- 게시일시
- 메뉴판 유형
- 메뉴 등록 방식: Gemini 분석 또는 직접 입력
- 등록된 메뉴 수
- 최종 메뉴 목록
- 새 메뉴판 등록 버튼
저장된 메뉴판 이미지가 존재하는 것처럼 표시하지 마세요.
게시일시는 Firestore의 publishedAt Timestamp를
한국 날짜 형식으로 표시해 주세요.
예:
게시자: 쥐피티
게시일시: 2026. 08. 02. 14:15
등록 방식: Gemini 메뉴판 분석
등록 메뉴: 57개
## 11. server.ts 점검
기존 /api/analyze-menu 엔드포인트는 유지해 주세요.
다음 내용을 확인해 주세요.
- 이미지 Base64를 디스크에 저장하지 않음
- Base64 전체를 로그로 출력하지 않음
- Gemini 응답 후 이미지 데이터를 별도로 보관하지 않음
- 응답에는 메뉴 분석 JSON만 포함
- API Key를 클라이언트에 노출하지 않음
- 잘못된 이미지와 요청 크기 초과 오류 처리
- Gemini 오류 시 적절한 HTTP 상태와 메시지 반환
최대 이미지 크기가 10MB이므로 Base64 증가분을 고려하여
Express JSON 요청 크기 제한을 안전한 범위로 설정해 주세요.
예를 들어 15MB보다 충분히 큰 합리적인 제한을 사용하되,
불필요하게 무제한으로 열지 마세요.
## 12. Firestore Security Rules
현재 이미지 저장 필드를 제거한 Firestore 구조에 맞게
firestore.rules를 수정해 주세요.
정책:
- restaurants, menuBoards, menuItems 공개 읽기 허용
- 인증 없이 새 문서 생성 허용
- 기존 문서 수정과 삭제 금지
- 허용하지 않은 추가 필드 차단
- 필수 필드와 자료형 검증
- publisherNickname 길이 검증
- boardType 값 검증
- analysisSource는 GEMINI 또는 MANUAL만 허용
- menuCount는 1 이상의 정수
- 메뉴 가격은 null 또는 0 이상의 숫자
- displayOrder는 1 이상의 정수
- 다른 컬렉션 접근 금지
Firebase Storage Rules는 더 이상 사용하지 않습니다.
## 13. 완료 조건
다음 조건을 모두 만족해야 합니다.
- Firebase Storage 관련 코드가 제거됨
- storage/no-default-bucket 오류가 발생하지 않음
- 메뉴판 이미지가 Gemini 분석에만 사용됨
- 이미지가 Firestore나 외부 저장소에 저장되지 않음
- 사용자 확인 중에는 로컬 이미지 미리보기가 유지됨
- 게시 성공 후 File과 Object URL이 정리됨
- Firestore에는 식당, 메뉴판 메타데이터, 메뉴 텍스트만 저장됨
- 이미지 전용 게시 기능이 제거됨
- 직접 입력 게시가 가능함
- Gemini 분석 게시가 가능함
- 저장 실패 시 입력 데이터와 이미지가 유지됨
- 기존 기능 1-A, 1-B, 1-C가 정상 유지됨
- 빌드 오류와 콘솔 오류가 없음
작업이 끝나면 다음 내용만 보고하고 중단해 주세요.
1. 생성하거나 수정한 파일
2. 제거한 Firebase Storage 관련 코드
3. 이미지의 생성부터 폐기까지의 생명주기
4. 변경된 Firestore 문서 구조
5. 이미지 전용 게시 제거 내용
6. Firestore 저장 및 실패 처리 흐름
7. 통합 테스트 방법

실제 게시가 되지 않았음
현재 whichMenu에서 Firestore 게시 과정 중 WebSocket 또는 네트워크 관련 오류가 발생합니다.
기존 Gemini 분석, 메뉴 편집, Firestore 데이터 구조,
firestore.rules 및 UI 기능을 임의로 변경하지 마세요.
먼저 오류 원인만 진단한 뒤, 필요한 경우에만 최소 범위로 수정해 주세요.
## 1. 오류 출처 구분
브라우저 콘솔과 Network 로그를 기준으로 다음 중 어디에서 발생한 오류인지 구분해 주세요.
A. Vite 개발 서버 HMR WebSocket
- [vite] failed to connect to websocket
- WebSocket closed without opened
- HMR connection lost
B. Firestore WebChannel 또는 Firestore 네트워크
- WebChannelConnection
- Listen stream transport errored
- Write stream transport errored
- ERR_CONNECTION_RESET
- client is offline
- Firestore RPC 오류
C. Firestore Security Rules
- permission-denied
- Missing or insufficient permissions
D. Firestore 설정
- project-not-found
- failed-precondition
- 잘못된 projectId 또는 Firebase 설정
먼저 어느 유형인지 보고하고, 근거가 되는 정확한 오류 코드와 발생 파일을 알려 주세요.
## 2. Vite HMR 오류인 경우
Vite HMR WebSocket 오류라면 Firebase 코드, Firestore 초기화,
저장 로직과 Security Rules를 수정하지 마세요.
다음만 확인해 주세요.
- Express와 Vite 개발 서버 연결 방식
- server.ts의 Vite middleware 설정
- HMR WebSocket 경로가 프록시 환경과 충돌하는지
- 동일한 개발 서버가 중복 실행되고 있지 않은지
- 미리보기 런타임 재시작이 필요한지
HMR 오류가 Firestore writeBatch 실패와 직접 관련이 없다면
그 사실을 명확하게 설명해 주세요.
## 3. Firestore WebChannel 오류인 경우
Firestore 네트워크 전송 오류로 확인된 경우에만
src/firebase.ts의 Firestore 초기화를 점검해 주세요.
현재 getFirestore(app)를 사용하고 있다면,
AI Studio 미리보기 또는 프록시 환경에서 연결 문제를 줄이기 위해
다음 설정이 필요한지 검토해 주세요.
initializeFirestore(app, {
experimentalAutoDetectLongPolling: true
});
주의사항:
- getFirestore와 initializeFirestore를 동시에 사용하지 않음
- Firestore를 최초로 사용하기 전에 한 번만 초기화
- Firebase App을 다시 initializeApp 하지 않음
- db 인스턴스를 여러 파일에서 중복 생성하지 않음
- 일시적인 연결 문제를 숨기기 위해 무조건 강제 long polling을 적용하지 않음
- autoDetect로 해결되지 않을 때만 experimentalForceLongPolling을 검토
- 기존 Firestore 문서 구조와 writeBatch 로직은 변경하지 않음
## 4. 저장 오류 표시 개선
게시 실패 시 사용자 화면과 콘솔에 다음 정보만 표시해 주세요.
- Firebase 오류 code
- Firebase 오류 message
- 실패 단계: VALIDATING 또는 SAVING
- 네트워크 연결 상태 navigator.onLine
API Key, 전체 Firebase 설정값, 메뉴 전체 데이터는 로그에 출력하지 마세요.
## 5. 결과
다음 순서로 보고해 주세요.
1. 오류 유형
2. 정확한 오류 코드
3. Firestore 게시 실패와의 직접적인 관계
4. 코드 수정이 필요한지 여부
5. 수정한 경우 수정 파일과 최소 변경 내용
6. AI Studio 미리보기 런타임 재시작이 필요한지 여부
새 기능을 추가하거나 기능 2를 구현하지 말고,
WebSocket 및 Firestore 게시 오류만 점검한 후 중단해 주세요.
firebase console에 데이터 없음 문제, 아래 프로젝트는 어제 만든 방명록 프로젝트이므로,,,,라고 생각했는데

정상 개발 이후에 firestore 들어가 보니까 요렇게 뜸

아까까지는 없었던 것이 맞긴 하지만, 프로젝트가 두개 생기는 구조가 아니라 데이터베이스가 두개 생기는 구조로 생성되었음을 확인 할 수 있음

현재 whichMenu 앱에는 Firebase SDK 코드와 Firestore 저장 코드가 있지만,
Firebase Console에서 이 앱과 연결된 Firebase 프로젝트 또는
Cloud Firestore 데이터베이스를 확인할 수 없습니다.
코드만 수정하지 말고 Google AI Studio의 공식 Firebase Integration을 통해
실제 Firebase 프로젝트와 Cloud Firestore 데이터베이스를
생성하거나 연결해 주세요.
## 1. 현재 연결 상태 먼저 진단
다음을 확인해 주세요.
- 이 AI Studio 앱에 Firebase Integration이 실제로 활성화되어 있는지
- 연결된 Google Cloud / Firebase projectId가 존재하는지
- 실제 Cloud Firestore 데이터베이스가 프로비저닝되어 있는지
- 현재 src/firebase.ts가 실제 연결 프로젝트의 설정을 사용하는지
- firebase.ts와 firestore.rules 파일만 생성되고
실제 클라우드 프로젝트는 생성되지 않은 상태인지
추측으로 성공 처리하지 말고 실제 Integration 상태를 기준으로 판단해 주세요.
## 2. Firebase 통합 시작
실제 Firebase 연결이 없다면
Google AI Studio의 Firebase Firestore Integration 설정 카드를 표시해 주세요.
사용자가 다음 내용을 직접 선택하고 승인할 수 있도록 해 주세요.
- 새 Firebase 프로젝트 생성 또는 기존 프로젝트 선택
- Firestore 데이터베이스 위치 선택
- Enable Firebase 권한 승인
사용자 승인 없이 Firebase가 생성된 것처럼 처리하지 마세요.
## 3. 프로젝트 선택
기존 프로젝트가 연결되어 있지 않다면
whichMenu 전용 새 Firebase 프로젝트를 사용해 주세요.
기존 프로젝트를 선택해야 한다면,
프로젝트 ID를 임의로 추측하지 말고
사용자가 Integration 설정 화면에서 선택하도록 해 주세요.
Firestore 위치는 최초 설정 후 변경하기 어려우므로
사용자에게 선택 내용을 확인받아 주세요.
## 4. Firestore만 앱 기능에 사용
whichMenu 앱은 현재 로그인 없는 MVP입니다.
다음 기능을 추가하지 마세요.
- 로그인 화면
- 회원가입
- Google 로그인 버튼
- Firebase Authentication 기반 사용자 소유권
- userId, uid, createdBy, ownerId 필드
Firebase Integration 과정에서 Authentication 서비스가 함께 구성되더라도,
현재 앱 코드와 Firestore 저장 로직에서는 Authentication을 사용하지 마세요.
기존 게시자 닉네임은 표시용 문자열이며 사용자 인증 정보가 아닙니다.
## 5. 기존 Firestore 구조 유지
실제 Firestore 프로젝트가 연결된 후 다음 컬렉션을 사용해 주세요.
- restaurants
- menuBoards
- menuItems
현재의 writeBatch 게시 구조를 유지해 주세요.
이미지는 저장하지 않으며,
최종 확인된 메뉴 텍스트만 Firestore에 저장합니다.
Firebase Storage는 추가하지 마세요.
## 6. 실제 설정값 연결
Firebase Integration이 완료되면 다음을 점검해 주세요.
- src/firebase.ts가 연결된 프로젝트의 실제 Firebase 설정을 사용
- projectId가 연결된 프로젝트와 일치
- Firebase App 중복 초기화 없음
- Firestore db 인스턴스 하나만 사용
- initializeFirestore의 long polling 설정 유지
- placeholder, 임시값, 예시 API Key가 남아 있지 않음
Firebase API Key 전체를 화면이나 콘솔에 출력하지 마세요.
## 7. Firestore Rules 연결
현재 로그인 없는 MVP 정책을 유지해 주세요.
- restaurants, menuBoards, menuItems 공개 읽기 허용
- 인증 없이 신규 문서 생성 허용
- 기존 문서 수정 및 삭제 금지
- 허용 필드와 자료형 검증
- 다른 컬렉션 접근 금지
firestore.rules 파일만 수정하는 것으로 끝내지 말고,
실제 연결된 Firestore 데이터베이스에 Rules가 반영되었는지 확인해 주세요.
보안 규칙을 전체 공개 형태로 완화하지 마세요.
## 8. 연결 확인 테스트
Firebase 연결 완료 후 테스트용 가짜 데이터를 만들지 말고,
현재 whichMenu 게시 흐름으로 실제 등록을 한 번 검증해 주세요.
확인 항목:
1. writeBatch commit 성공
2. restaurants 문서 생성
3. menuBoards 문서 생성
4. menuItems 문서 생성
5. 각 restaurantId와 menuBoardId 연결 확인
6. Firebase Console에서 생성된 문서 확인 가능
7. 앱에서 해당 데이터를 다시 조회할 수 있는 상태인지 확인
## 9. 완료 보고
다음 내용만 명확히 보고해 주세요.
1. 연결된 Firebase projectId
2. Firestore databaseId
3. 데이터베이스 위치
4. Firebase Integration 활성화 여부
5. 실제 Firebase Console에서 확인할 경로
6. 생성되거나 수정된 파일
7. Firestore Rules 반영 여부
8. 사용자가 아직 직접 승인해야 하는 단계
실제 프로젝트나 데이터베이스 생성이 완료되지 않았다면
완료됐다고 보고하지 말고,
사용자가 눌러야 할 버튼과 설정 위치를 안내한 뒤 중단해 주세요.

수정 후 답변
1. 연결된 Firebase projectId
- seraphic-water-3n50x
2. Firestore databaseId
- ai-studio-whichmenu-52fdc6de-449a-4524-8197-1038ea822a4d
3. 데이터베이스 위치
- asia-northeast1 (AI Studio 기본 클라우드 리전)
4. Firebase Integration 활성화 여부
- 활성화 완료 (set_up_firebase 툴을 통한 실제 프로비저닝 완료)
5. 실제 Firebase Console에서 확인할 경로
- Firebase Console → 프로젝트: seraphic-water-3n50x → Firestore Database
- 데이터베이스 선택: ai-studio-whichmenu-52fdc6de-449a-4524-8197-1038ea822a4d
- 컬렉션 경로: restaurants, menuBoards, menuItems
6. 생성되거나 수정된 파일
- firebase-applet-config.json: 프로비저닝된 실제 프로젝트 키 및 firestoreDatabaseId 설정 저장
- src/firebase.ts: firebase-applet-config.json을 직접 참조하여 실제 Firebase App 및 initializeFirestore (experimentalAutoDetectLongPolling: true, 지정된 databaseId)로 바인딩
- firestore.rules: restaurants, menuBoards, menuItems 보안 규칙 정의
7. Firestore Rules 반영 여부
- 반영 완료 (deploy_firebase 툴을 사용하여 실제 연결된 Firestore 프로젝트에 firestore.rules 배포 완료)
8. 사용자가 아직 직접 승인해야 하는 단계
- 없음: UI 승인이 정상 완료되어 프로비저닝이 성공했습니다. 이제 whichMenu 앱에서 메뉴 등록 후 게시를 진행하시면 Cloud Firestore에 데이터가 실제 저장됩니다.

조회 화면 만드는 프롬프트
# whichMenu 기능 2-A 구현: Firestore 게시 메뉴 목록 및 상세 조회
현재 whichMenu 기능 1-A부터 1-D까지 구현되어 있습니다.
사용자는 메뉴판 이미지를 Gemini로 분석하고,
분석 결과를 수정한 뒤 다음 Cloud Firestore 컬렉션에
식당과 메뉴 정보를 게시할 수 있습니다.
- restaurants
- menuBoards
- menuItems
이번 작업에서는 Firestore에 실제 게시된 데이터를
whichMenu 앱에서 조회할 수 있는 목록 화면과 상세 화면만 구현해 주세요.
검색, AI 추천, 수정, 삭제 기능은 아직 구현하지 마세요.
---
## 1. 기존 Firebase 연결 유지
현재 Firebase Integration을 통해 다음 프로젝트와
Firestore 데이터베이스가 실제로 연결되어 있습니다.
projectId:
seraphic-water-3n50x
databaseId:
ai-studio-whichmenu-52fdc6de-449a-4524-8197-1038ea822a4d
현재 다음 설정 파일을 그대로 사용해 주세요.
- firebase-applet-config.json
- src/firebase.ts
- firestore.rules
중요 조건:
- 새 Firebase 프로젝트를 생성하지 않음
- 새 Firestore 데이터베이스를 생성하지 않음
- Firebase App을 다시 initializeApp 하지 않음
- getFirestore와 initializeFirestore를 혼용하지 않음
- 현재 export된 단일 db 인스턴스를 재사용
- experimentalAutoDetectLongPolling 설정 유지
- databaseId를 임의로 변경하거나 `(default)`로 변경하지 않음
- Firebase Storage를 추가하지 않음
- Firebase Authentication을 추가하지 않음
---
## 2. 기존 등록 기능 유지
다음 기능은 수정하거나 제거하지 마세요.
- 식당 정보 입력
- 게시자 닉네임 입력
- 메뉴판 이미지 선택 및 미리보기
- Gemini 메뉴판 이미지 분석
- Gemini 서버 API `/api/analyze-menu`
- 메뉴 확인·수정·추가·삭제·순서 변경
- 직접 메뉴 입력
- Firestore writeBatch 게시
- 게시 완료 화면
- 메뉴판 이미지 비저장 정책
- GEMINI / MANUAL 등록 방식
- 기존 Firestore Security Rules
등록 기능을 조회 기능으로 대체하지 말고 함께 사용할 수 있게 해 주세요.
---
## 3. 라우팅 구성
기존 라우팅이 있다면 그대로 확장해 주세요.
라우팅이 없다면 React Router를 사용하여 최소한으로 구성해 주세요.
권장 경로:
- `/` : 게시된 메뉴 목록
- `/register` : 기존 메뉴판 등록 화면
- `/menu-boards/:menuBoardId` : 게시된 메뉴 상세 화면
공통 Header에 다음 메뉴를 제공해 주세요.
- 메뉴 보기
- 메뉴 등록
브라우저 새로고침 후에도 상세 URL이 정상적으로 열려야 합니다.
---
## 4. Firestore 데이터 구조
현재 저장 구조를 그대로 사용해 주세요.
### restaurants/{restaurantId}
{
name: string;
address: string;
category: string;
description: string | null;
createdAt: Timestamp;
updatedAt: Timestamp;
}
### menuBoards/{menuBoardId}
{
restaurantId: string;
boardType: "STATIC" | "DAILY";
menuDate: string | null;
publisherNickname: string;
analysisSource: "GEMINI" | "MANUAL";
menuCount: number;
publishedAt: Timestamp;
createdAt: Timestamp;
updatedAt: Timestamp;
}
### menuItems/{menuItemId}
{
restaurantId: string;
menuBoardId: string;
name: string;
price: number | null;
description: string | null;
category: string | null;
displayOrder: number;
createdAt: Timestamp;
updatedAt: Timestamp;
}
필드명을 변경하거나 새로운 중복 필드를 기존 문서에 추가하지 마세요.
---
## 5. TypeScript 조회 타입 정의
Firestore 원본 타입과 화면에서 사용할 조합 타입을 구분해 주세요.
예시:
type QueryStatus =
| "IDLE"
| "LOADING"
| "SUCCESS"
| "EMPTY"
| "FAILED";
interface PublishedMenuSummary {
menuBoardId: string;
restaurantId: string;
restaurantName: string;
address: string;
restaurantCategory: string;
boardType: "STATIC" | "DAILY";
menuDate: string | null;
publisherNickname: string;
analysisSource: "GEMINI" | "MANUAL";
menuCount: number;
publishedAt: Date | null;
}
interface PublishedMenuDetail {
menuBoardId: string;
restaurantId: string;
restaurant: Restaurant;
menuBoard: MenuBoard;
menuItems: MenuItem[];
}
Firestore Timestamp를 UI 컴포넌트에 그대로 전달하지 말고
조회 서비스 또는 변환 함수에서 Date로 안전하게 변환해 주세요.
---
## 6. Firestore 조회 서비스 분리
Firestore 조회 코드를 React UI 컴포넌트에 직접 길게 작성하지 말고
별도 서비스 파일로 분리해 주세요.
권장 파일:
src/services/menuQueryService.ts
다음 함수를 구현해 주세요.
### getLatestMenuBoards
역할:
- menuBoards 컬렉션 조회
- publishedAt 내림차순
- 최초 20개 조회
- 페이지 커서 지원
- Restaurant 정보 결합
- 다음 페이지 존재 여부 반환
예상 반환 구조:
{
items: PublishedMenuSummary[];
lastDocument: DocumentSnapshot | null;
hasMore: boolean;
}
조회 조건:
query(
collection(db, "menuBoards"),
orderBy("publishedAt", "desc"),
limit(20)
)
다음 페이지는 startAfter(lastDocument)를 사용해 주세요.
실시간 리스너 onSnapshot은 사용하지 마세요.
getDocs 기반의 일회성 조회를 사용해 주세요.
### getMenuBoardDetail
menuBoardId를 받아 다음 데이터를 조회해 주세요.
1. menuBoards/{menuBoardId}
2. 연결된 restaurants/{restaurantId}
3. menuItems에서 menuBoardId가 같은 메뉴 목록
메뉴 조회 조건:
where("menuBoardId", "==", menuBoardId)
orderBy("displayOrder", "asc")
반환:
PublishedMenuDetail | null
MenuBoard가 존재하지 않으면 null을 반환해 주세요.
---
## 7. 목록 조회 시 Restaurant 결합 최적화
Firestore는 관계형 데이터베이스의 JOIN을 지원하지 않으므로
클라이언트에서 데이터를 조합해 주세요.
목록의 menuBoards를 조회한 뒤:
1. restaurantId 목록 추출
2. 중복 restaurantId 제거
3. Restaurant 문서를 Promise.all로 병렬 조회
4. Map<restaurantId, Restaurant>으로 구성
5. MenuBoard와 Restaurant 결합
Restaurant를 다음처럼 순차 조회하지 마세요.
for (...) {
await getDoc(...)
}
중복된 restaurantId는 한 번만 조회해야 합니다.
Restaurant 문서가 누락된 경우 전체 목록을 실패시키지 말고
해당 게시물에 다음과 같은 대체값을 표시해 주세요.
- 식당 정보 없음
- 카테고리 미확인
---
## 8. 게시 메뉴 목록 화면
권장 파일:
src/pages/MenuBoardListPage.tsx
또는 기존 프로젝트 구조에 맞는 동등한 파일을 사용해 주세요.
페이지 제목:
게시된 메뉴
각 게시물은 카드 형태로 표시해 주세요.
표시 정보:
- 식당명
- 주소
- 음식 카테고리
- 메뉴판 유형
- 메뉴 적용 날짜
- 게시자 닉네임
- 게시일시
- 등록 방식
- 등록 메뉴 수
표시 규칙:
- STATIC: 상시 메뉴
- DAILY: 오늘의 메뉴 또는 일일 메뉴
- DAILY인 경우 menuDate 표시
- GEMINI: Gemini 분석
- MANUAL: 직접 입력
- publishedAt이 null이면 날짜 확인 중 또는 등록 일시 미확인
- 가격이나 이미지 미리보기는 목록에서 조회하지 않음
카드를 클릭하면 다음 상세 경로로 이동해 주세요.
/menu-boards/{menuBoardId}
---
## 9. 목록 상태 처리
다음 상태를 각각 구현해 주세요.
### LOADING
- 스켈레톤 또는 로딩 표시
- 기존 목록이 있는 상태에서 추가 조회할 때 전체 화면을 비우지 않음
### EMPTY
문구:
아직 게시된 메뉴가 없습니다.
버튼:
첫 메뉴 등록하기
버튼을 누르면 `/register`로 이동합니다.
### FAILED
문구:
게시된 메뉴를 불러오지 못했습니다.
제공 기능:
- 다시 불러오기
- 메뉴 등록 화면 이동
### SUCCESS
- 목록 표시
- hasMore가 true이면 `더 보기` 버튼 표시
- 더 보기를 누르면 startAfter를 이용해 다음 20개 추가
- 중복 게시물이 목록에 추가되지 않도록 menuBoardId 기준으로 병합
---
## 10. 메뉴 상세 화면
권장 파일:
src/pages/MenuBoardDetailPage.tsx
URL의 menuBoardId로 게시물을 조회해 주세요.
상세 화면의 상단에는 Restaurant 정보를 표시해 주세요.
- 식당명
- 주소
- 음식 카테고리
- 식당 설명
MenuBoard 정보:
- 게시자 닉네임
- 게시일시
- 메뉴판 유형
- 메뉴 적용 날짜
- 등록 방식
- 등록된 메뉴 수
MenuItem 목록:
- 메뉴명
- 가격
- 설명
- 메뉴 카테고리
- 표시 순서
메뉴는 displayOrder 오름차순으로 표시해 주세요.
가격 표시 규칙:
- 숫자인 경우 한국 원화 형식
- 예: 12000 → 12,000원
- null인 경우 `가격 정보 없음`
- 0인 경우 0원으로 정상 표시
설명이나 카테고리가 null이면 불필요한 빈 행을 표시하지 마세요.
메뉴판 이미지는 저장하지 않으므로 이미지 영역을 만들지 마세요.
---
## 11. 상세 화면 상태 처리
다음 상태를 구분해 주세요.
- LOADING
- SUCCESS
- NOT_FOUND
- FAILED
### NOT_FOUND
문구:
존재하지 않거나 확인할 수 없는 게시물입니다.
버튼:
목록으로 돌아가기
### FAILED
문구:
메뉴 정보를 불러오지 못했습니다.
기능:
- 다시 불러오기
- 목록으로 돌아가기
Restaurant가 누락되었더라도 MenuBoard와 MenuItem을 조회할 수 있다면
메뉴 목록은 표시하고 식당 정보만 대체 문구로 처리해 주세요.
---
## 12. 게시 완료 화면과 상세 조회 연결
기존 게시 완료 화면에 다음 버튼을 추가해 주세요.
- 게시물 보기
- 게시 목록으로 이동
- 새 메뉴판 등록
`게시물 보기` 버튼은 방금 생성한 menuBoardId를 사용하여
다음 경로로 이동해야 합니다.
/menu-boards/{menuBoardId}
이를 위해 게시 함수가 성공 결과로 다음 값을 제공하도록 해 주세요.
{
restaurantId: string;
menuBoardId: string;
publishedAt: Date | null;
}
기존 Firestore 저장 구조와 writeBatch 처리 순서는 변경하지 마세요.
---
## 13. 날짜 표시
Firestore Timestamp를 한국어 날짜 형식으로 표시해 주세요.
예:
2026. 08. 02. 14:54
공통 유틸리티 함수를 만들어 중복 구현하지 마세요.
권장 파일:
src/utils/dateFormat.ts
null, 잘못된 Timestamp, 변환할 수 없는 값에도
화면이 중단되지 않도록 처리해 주세요.
---
## 14. 가격 표시
공통 가격 포맷 함수를 구현해 주세요.
권장 파일:
src/utils/priceFormat.ts
규칙:
- 12000 → 12,000원
- 0 → 0원
- null → 가격 정보 없음
- NaN 또는 잘못된 값 → 가격 정보 확인 필요
---
## 15. Firestore 인덱스
다음 조회에 필요한 Firestore 인덱스를 확인해 주세요.
1. menuBoards
- publishedAt 내림차순
2. menuItems
- menuBoardId 동일 조건
- displayOrder 오름차순
복합 인덱스가 필요하면:
- firestore.indexes.json을 생성 또는 수정
- 현재 연결된 Firebase 프로젝트와 지정 databaseId에 배포
- 임의로 쿼리를 제거하거나 전체 데이터를 조회한 뒤 필터링하지 않음
인덱스가 이미 존재하면 중복 생성하지 마세요.
필요한 인덱스와 배포 결과를 완료 보고에 포함해 주세요.
---
## 16. Firestore Security Rules
현재 배포된 Rules에서 다음 읽기가 허용되는지 확인해 주세요.
- restaurants read
- menuBoards read
- menuItems read
이미 공개 읽기가 허용되어 있다면 규칙을 수정하지 마세요.
조회 오류를 해결한다는 이유로 다음과 같은 전체 공개 규칙을 추가하지 마세요.
match /{document=**} {
allow read, write: if true;
}
신규 문서 생성 정책과 기존 수정·삭제 금지 정책을 유지해 주세요.
---
## 17. 조회 비용과 요청 수 최소화
비용과 무료 할당량을 고려하여 다음 원칙을 지켜 주세요.
- 실시간 onSnapshot 사용 금지
- getDocs 및 getDoc 기반 일회성 조회
- 최초 목록은 20개만 조회
- 더 보기 방식 페이지네이션
- Restaurant 중복 조회 방지
- 목록에서 MenuItem 전체를 조회하지 않음
- 상세 화면에 진입했을 때만 MenuItem 조회
- 동일 버튼의 중복 클릭 방지
- React StrictMode 개발 환경에서 요청이 중복 실행되지 않도록 점검
- 무한 재조회 또는 useEffect 루프 방지
목록 카드마다 MenuItem을 조회하여 대표 메뉴를 표시하지 마세요.
---
## 18. 오류 로깅
Firestore 조회 오류가 발생하면 다음 정보만 기록해 주세요.
- 오류 code
- 오류 message
- 실패한 조회 단계
- navigator.onLine
다음 정보는 출력하지 마세요.
- Firebase API Key
- 전체 Firebase 설정
- 전체 Restaurant 데이터
- 전체 MenuItem 데이터
- 사용자 입력 원문
사용자 화면에는 원본 Firebase 오류 JSON을 그대로 출력하지 말고
이해하기 쉬운 안내 문구를 표시해 주세요.
---
## 19. 반응형 UI
기존 whichMenu 디자인 스타일을 유지해 주세요.
목록 화면:
- 모바일: 카드 한 열
- 태블릿: 두 열
- 데스크톱: 두 열 또는 세 열
상세 화면:
- 식당 정보 영역
- 게시 정보 영역
- 메뉴 목록 영역
이미지가 존재하는 것처럼 빈 이미지 박스를 만들지 마세요.
접근성 조건:
- 카드에 키보드 포커스 가능
- 버튼에 명확한 label
- 로딩 상태에 aria-live 또는 적절한 안내
- 클릭 가능한 div보다 button 또는 Link 사용
---
## 20. 이번 작업에서 제외할 범위
다음 기능은 추가하지 마세요.
- 메뉴 검색
- 식당명 검색
- 카테고리 필터
- 가격 필터
- AI 메뉴 추천
- 위치 기반 조회
- 지도
- 좋아요
- 댓글
- 조회수
- 게시물 수정
- 게시물 삭제
- 로그인
- 회원가입
- Firebase Authentication
- 이미지 저장
- Firebase Storage
- 기존 식당 중복 병합
이 기능들은 기능 2-B 이후에 별도로 구현합니다.
---
## 21. 통합 테스트
다음 시나리오를 검증해 주세요.
### 목록 조회
1. 게시 데이터가 없을 때 EMPTY 화면
2. 게시 데이터가 한 개일 때 정상 카드 표시
3. 여러 게시물을 publishedAt 내림차순으로 표시
4. 더 보기 버튼으로 다음 페이지 조회
5. 중복된 menuBoardId가 목록에 생기지 않음
### 상세 조회
1. 정상 menuBoardId 조회
2. 연결된 Restaurant 표시
3. MenuItem을 displayOrder 순서로 표시
4. price가 null인 메뉴 처리
5. description과 category가 null인 메뉴 처리
6. 존재하지 않는 menuBoardId 처리
7. Restaurant 문서가 누락된 경우 처리
### 등록 연동
1. 새 메뉴판 게시
2. 게시 완료 화면에서 `게시물 보기`
3. 방금 등록한 상세 화면 이동
4. 게시 목록에서 방금 등록한 게시물 확인
### 네트워크 및 오류
1. 오프라인 상태 조회 실패 처리
2. Firestore permission-denied 처리
3. 필요한 인덱스 누락 처리
4. 다시 불러오기 동작
5. 중복 네트워크 요청 여부 확인
---
## 22. 완료 조건
다음 조건을 모두 만족해야 합니다.
- 기존 메뉴 등록 기능이 정상 유지됨
- Firestore 게시 데이터가 앱 목록에 표시됨
- 목록은 publishedAt 최신순으로 정렬됨
- 최초 20개만 조회됨
- 더 보기 페이지네이션이 동작함
- 메뉴판 상세 화면이 동작함
- Restaurant, MenuBoard, MenuItem이 올바르게 조합됨
- 메뉴가 displayOrder 순서로 표시됨
- 등록 완료 후 해당 게시물 상세로 이동 가능함
- 로딩, 빈 상태, 실패, 존재하지 않음 상태가 처리됨
- Firestore 읽기 요청이 불필요하게 반복되지 않음
- 기존 Firebase 프로젝트와 databaseId가 유지됨
- Firebase Storage와 Authentication이 추가되지 않음
- 빌드 오류와 TypeScript 오류가 없음
- 브라우저 콘솔에 불필요한 오류가 없음
작업 완료 후 다음 내용만 보고하고 중단해 주세요.
1. 생성하거나 수정한 파일
2. 추가한 페이지와 URL 경로
3. Firestore 목록 조회 쿼리
4. Firestore 상세 조회 쿼리
5. Restaurant, MenuBoard, MenuItem 조합 방식
6. 페이지네이션 구현 방식
7. 게시 완료 화면과 상세 화면 연결 방식
8. 생성하거나 배포한 Firestore 인덱스
9. Firestore Rules 변경 여부
10. 통합 테스트 결과


결과 프롬프트
whichMenu 기능 2-A(Cloud Firestore 게시 메뉴 목록 및 상세 조회) 구현을 완료했습니다.
1. 생성 및 수정된 파일
- src/types.ts: 조회 전용 상태(QueryStatus) 및 조합 인터페이스(PublishedMenuSummary, PublishedMenuDetail, FirestoreRestaurant, FirestoreMenuBoard, FirestoreMenuItem) 추가
- src/utils/dateFormat.ts: Firestore Timestamp / Date 객체를 안전하게 한국어 날짜 형식(YYYY. MM. DD. HH:mm)으로 변환하는 공통 유틸리티 작성
- src/utils/priceFormat.ts: 가격 포맷 유틸리티 작성 (12,000원, 0원, 가격 정보 없음)
- src/services/menuQueryService.ts: Firestore 최신 게시 목록 조회(getLatestMenuBoards) 및 상세 조회(getMenuBoardDetail) 전용 서비스 함수 분리
- src/pages/MenuBoardListPage.tsx: 게시된 메뉴 목록 화면 컴포넌트 (반응형 카드 그리드, 로딩/빈 상태/실패/성공 상태 및 '더 보기' 커서 페이지네이션 구현)
- src/pages/MenuBoardDetailPage.tsx: 게시된 메뉴 상세 화면 컴포넌트 (식당 정보, 게시 정보, displayOrder 순서별 메뉴 목록 표시 및 로딩/NOT_FOUND/실패/성공 상태 처리)
- src/pages/RegisterPage.tsx: 기존 메뉴판 입력 및 Gemini AI 분석/수정/게시 등록 흐름을 캡슐화한 등록 페이지
- src/components/menu-registration/Header.tsx: 상단 공통 헤더에 메뉴 보기 (/), 메뉴 등록 (/register) 네비게이션 탭 구현
- src/components/menu-registration/PublishCompleteView.tsx: 게시 완료 화면에 게시물 보기 (/menu-boards/:menuBoardId), 게시 목록으로 이동 (/), 새 메뉴판 등록 버튼 추가
- src/App.tsx: react-router-dom 기반 라우터 구성
- firestore.indexes.json: menuItems 컬렉션의 menuBoardId ASC + displayOrder ASC 복합 인덱스 정의 및 배포
2. 추가된 페이지 및 URL 경로
- / : 게시된 메뉴 목록 화면 (MenuBoardListPage)
- /register : 기존 메뉴판 등록 및 Gemini AI 분석 화면 (RegisterPage)
- /menu-boards/:menuBoardId : 게시 메뉴 상세 화면 (MenuBoardDetailPage)
3. Firestore 목록 조회 쿼리
query(
collection(db, "menuBoards"),
orderBy("publishedAt", "desc"),
limit(20)
)
- 다음 페이지 조회 시: startAfter(lastDocument) 적용
4. Firestore 상세 조회 쿼리
- doc(db, "menuBoards", menuBoardId)
- doc(db, "restaurants", restaurantId)
- query(collection(db, "menuItems"), where("menuBoardId", "==", menuBoardId), orderBy("displayOrder", "asc"))
5. Restaurant, MenuBoard, MenuItem 조합 방식
- 목록 조회:
- menuBoards 20개 문서를 한번에 조회
- 문서들에서 중복되지 않은 restaurantId 목록 추출
- Promise.all을 이용해 해당 restaurant 문서들을 병렬로 일회성 조회 후 Map<restaurantId, Restaurant> 구성 (순차 await 조회를 방지하여 네트워크 요청 수 최소화)
- 클라이언트에서 MenuBoard와 Restaurant 정보를 안전하게 조합. 식당 문서가 누락된 경우 대체 문구(식당 정보 없음, 카테고리 미확인) 처리
- 상세 조회:
- menuBoardId로 menuBoard 조회
- 연결된 restaurantId로 restaurant 조회
- menuBoardId가 일치하는 menuItems 문서를 displayOrder 오름차순으로 조회하여 PublishedMenuDetail 객체로 조합
6. 페이지네이션 구현 방식
- limit(20) 기준 일회성 getDocs 조회 사용 (onSnapshot 미사용)
- lastDocument 스냅샷을 저장하고, 더 보기 버튼 클릭 시 startAfter(lastDocument)를 활용하여 다음 20개 조회
- menuBoardId 기준으로 목록 상태를 병합하여 중복 항목 노출을 방지
7. 게시 완료 화면과 상세 화면 연결 방식
- PublishCompleteView에서 방금 저장 성공한 publishResult.menuBoardId를 이용하여:
- 게시물 보기: /menu-boards/${publishResult.menuBoardId} 경로로 이동
- 게시 목록으로 이동: / 경로로 이동
- 새 메뉴판 등록: 양식 초기화 후 등록 화면 유지
8. 생성 및 배포한 Firestore 인덱스
- firestore.indexes.json 파일 생성 및 deploy_firebase 완료:
- menuItems 컬렉션: menuBoardId (ASCENDING) + displayOrder (ASCENDING)
9. Firestore Rules 변경 여부
- 기존 firestore.rules 유지 (변경 없음).
- restaurants, menuBoards, menuItems에 대한 공개 읽기(read) 및 검증된 생성(create) 허용 규칙과 수정/삭제 금지 보안 정책이 온전히 작동합니다.
10. 통합 검증 결과
- lint_applet: 오류 없음 (tsc --noEmit 통과)
- compile_applet: 빌드 성공
- 기존 메뉴 등록 및 Gemini AI 분석/게시 기능 정상 유지
- Firestore 데이터 수집 및 상세 조회가 올바르게 연동됨
두번째 기능
후보 검색 + AI 재랭킹
첫번째 프롬프트
# whichMenu 기능 2-B 사전 진단: 일반 검색 + AI 추천 설계
현재 whichMenu에는 다음 기능이 구현되어 있습니다.
- 메뉴판 이미지 Gemini 분석
- 메뉴 수정 및 직접 입력
- Firestore 게시
- 게시 메뉴 목록 조회
- 게시 메뉴 상세 조회
Firestore 컬렉션:
- restaurants
- menuBoards
- menuItems
이번에는 아직 코드를 수정하지 말고,
일반 메뉴 검색과 AI 메뉴 추천을 추가하기 위한 현재 구조만 진단해 주세요.
목표 구조:
1. 일반 검색으로 실제 Firestore 메뉴 후보를 조회
2. 일반 검색 모드에서는 후보를 그대로 표시
3. AI 추천 모드에서는 검색된 후보만 Gemini에 전달
4. Gemini가 후보를 순위화하고 추천 이유를 반환
5. Gemini 할당량 소진, 서버 혼잡, 타임아웃 시 일반 검색 결과로 전환
다음을 확인해 주세요.
1. 현재 게시 목록 및 상세 조회 서비스 파일
2. menuItems 조회에 사용할 수 있는 기존 필드
3. Restaurant와 MenuBoard를 조합하는 기존 방식
4. 현재 라우팅 구조
5. 검색 페이지를 추가하기 적절한 경로
6. 일반 검색에 필요한 Firestore 쿼리와 인덱스
7. 메뉴명 부분 문자열 검색을 현재 Firestore 구조로 처리할 수 있는지
8. 데이터가 적은 MVP에서 제한된 문서를 조회한 뒤
클라이언트 필터링하는 방식이 적절한지
9. AI 추천에 전달할 최대 후보 개수
10. 기존 /api/analyze-menu와 별도로
/api/recommend-menu를 추가하는 것이 적절한지
11. 현재 server.ts의 Gemini 오류 처리와 재사용 가능한 부분
12. 수정이 필요한 파일과 새로 생성할 파일
다음 기능은 아직 구현하지 마세요.
- 검색 UI
- AI 추천 API
- Firestore 문서 구조 변경
- 새로운 Firebase 프로젝트 생성
- Authentication
- Firebase Storage
- 외부 검색 서비스
- 벡터 데이터베이스
진단 결과와 구현 계획만 보고하고 중단해 주세요.
진단 결과 및 구현 계획
whichMenu 기능 2-B: 일반 검색 + AI 추천 진단 결과 및 설계 보고서
요청하신 기능 2-B(일반 메뉴 검색 및 AI 추천) 추가를 위해 현재 codebase 구조를 다각도로 진단하고 구현 계획을 수립하였습니다. 현재 단계에서는 코드를 임의로 수정하거나 생성하지 않고 진단 결과와 설계안만 보고합니다.
1. 현재 게시 목록 및 상세 조회 서비스 파일
- 파일 경로: src/services/menuQueryService.ts
- 주요 함수:
- getLatestMenuBoards(): menuBoards 컬렉션을 publishedAt 내림차순 20개 단위로 조회하고, 포함된 restaurantId를 병렬로 일괄 조회하여 조합
- getMenuBoardDetail(): 단일 menuBoardId 기준 MenuBoard, Restaurant, MenuItem[](displayOrder 오름차순)을 일괄 조회
2. menuItems 조회에 사용할 수 있는 기존 필드
menuItems 컬렉션의 문서 필드:
- id (string)
- restaurantId (string)
- menuBoardId (string)
- name (string): 메뉴명
- price (number | null): 가격
- description (string | null): 메뉴 설명
- category (string | null): 메뉴 카테고리
- displayOrder (number)
- createdAt (Timestamp)
- updatedAt (Timestamp)
3. Restaurant와 MenuBoard를 조합하는 기존 방식
- menuItems 쿼리 결과에서 추출한 restaurantId 및 menuBoardId의 중복을 제거(Set)한 뒤, Promise.all 기반 병렬 getDoc으로 문서들을 가져와 Map<id, DocumentData>에 저장합니다.
- 추출된 문서들을 클라이언트 메모리 상에서 1:1, 1:N 관계로 안전하게 결합합니다. (문서가 누락된 경우에도 대체 텍스트 처리)
4. 현재 라우팅 구조
- src/App.tsx (React Router v6 기반):
- / : 게시된 메뉴 목록 (MenuBoardListPage)
- /register : 메뉴판 등록 (RegisterPage)
- /menu-boards/:menuBoardId : 메뉴판 상세 (MenuBoardDetailPage)
5. 검색 페이지를 추가하기 적절한 경로
- 신규 경로: /search (MenuSearchPage.tsx)
- Header.tsx 상단 네비게이션 탭에 "메뉴 검색" 아이콘 탭을 추가하고, /search 경로로 연결합니다.
6 & 7. Firestore 쿼리와 메뉴명 부분 문자열(Substring) 검색 한계
- Firestore 쿼리 한계: Firestore Native Query는 LIKE '%키워드%' 형태의 부분 문자열 포함(contains) 검색이나 전문 검색(Full-text search)을 지원하지 않으며, where('name', '>=', query) 문법으로는 오직 접두사(Prefix) 검색만 가능합니다.
- 현재 구조상의 처리 방안:
- Firestore 문서 구조를 변경하거나 Algolia/Typesense 등 외부 제3자 전문 검색 엔진을 도입하지 않기 위해, menuItems 컬렉션에서 최근 등록된 데이터 N개(예: 100~200개)를 getDocs로 조회합니다.
- 클라이언트 JavaScript에서 item.name, item.category, item.description, restaurant.name 필드에 대해 includes() 기반 대소문자/공백 무시 부분 문자열 필터링을 수행합니다.
8. 데이터가 적은 MVP에서 클라이언트 필터링 방식의 적절성
- 매우 적절함: 데이터 양이 적은 MVP 단계에서는 추가 비용이나 외부 의존성, Firestore 구조 개편 없이 완전한 중간 단어 포함 검색(예: "제육" 입력 시 "매콤제육덮밥" 검색)을 유연하게 제공할 수 있습니다.
9. AI 추천에 전달할 최대 후보 개수
- 최대 15개~20개 후보:
- 클라이언트 1차 검색으로 필터링된 후보 메뉴 중 상위 15개 내외를 Gemini에 전달합니다.
- 이유: Gemini API의 토큰 사용량을 최소화하고, 응답 시간을 단축하며, AI가 집중도 높은 맞춤형 순위 및 추천 사유를 생성하도록 유도하기 위함입니다.
10 & 11. /api/recommend-menu 엔드포인트 추가 및 server.ts 재사용
- 엔드포인트 분리 적절성: 기존 이미지 분석 API(/api/analyze-menu)와 역할이 완전히 다르므로 별도의 /api/recommend-menu POST 라우트를 추가하는 것이 깔끔합니다.
- server.ts 재사용 요소:
- @google/genai의 GoogleGenAI SDK 및 process.env.GEMINI_API_KEY 환경 변수
- gemini-2.5-flash 모델 및 responseMimeType: 'application/json', responseSchema 기법
- API Key 및 요청 데이터 검증 에러 핸들링
- Fallback 처리: Gemini API 응답 실패/타임아웃 발생 시 500 에러 대신 1차 검색 후보 목록을 기본 추천 결과로 반환하거나 클라이언트에서 일반 검색 결과로 안전하게 전환하도록 설계 가능.
12. 수정 및 새로 생성할 파일 목록 (향후 구현 시)
수정이 필요한 파일
- /server.ts : /api/recommend-menu API 라우트 추가
- /src/App.tsx : /search 라우트 등록
- /src/components/menu-registration/Header.tsx : 메뉴 검색 네비게이션 탭 추가
- /src/types.ts : 검색 조건 및 AI 추천 관련 TypeScript 인터페이스 추가
새로 생성할 파일
- /src/services/menuSearchService.ts : Firestore 1차 메뉴 검색 및 /api/recommend-menu 호출 서비스
- /src/pages/MenuSearchPage.tsx : 검색어 입력, 검색/AI 추천 모드 토글, 결과 목록 및 AI 추천 사유 표시 UI
진단 및 설계 검토를 마쳤으며, 지침에 따라 어떠한 코드 수정 없이 보고서를 제출하고 작업을 대기합니다.
현재 상태 보존 명령 프롬프트
현재 whichMenu의 메뉴 등록, Firestore 게시, 목록 조회, 상세 조회 기능이
정상 동작하는 상태를 체크포인트로 보존해 주세요.
이번 이후 작업은 기능 2-B 범위에서 진행합니다.
기존 기능을 임의로 리팩터링하거나 Firebase 프로젝트,
Firestore databaseId, Security Rules를 변경하지 마세요.
현재 상태 보존이 완료되면 수정 없이 완료 여부만 알려 주세요.

하하하 일일 한도에 도달했다....
퍼블리싱 중
My Google AI Studio App
whichmenu.ai.studio

프롬프트를 작성하며...
- 기능을 진단·구현·오류 처리·통합 테스트 단계로 나누어 요청했다.
- AI가 필요한 판단과 일반 코드가 담당할 데이터 처리를 분리했다.
- AI는 Firestore에 실제로 존재하는 메뉴 후보만 평가하도록 제한했다.
- AI 할당량이나 장애가 발생해도 일반 검색으로 서비스가 계속 동작하도록 설계했다.
- Firestore 조회량, Gemini 후보 수, 추천 결과 수에 상한을 두어 비용과 토큰 사용량을 통제했다.
- 이미지는 분석에만 일시적으로 사용하고, 검수된 메뉴 텍스트만 저장하도록 데이터 생명주기를 명시했다.
- API Key는 서버에서만 사용하고, AI 응답은 JSON Schema와 후처리 검증을 거치도록 했다.
- 오류를 일시적 장애, 일일 할당량, 설정 오류로 나누어 재시도 정책을 다르게 적용했다.
- 새 기능 구현 시 기존 Firebase 설정과 등록·조회 기능을 변경하지 않도록 범위를 제한했다.
- 구현 완료 조건과 테스트 항목, 결과 보고 형식까지 프롬프트에 포함했다.

번외 1 : .ts와 .tsx는 TypeScript의 파일 확장자이고, .tsx는 React에서 많이 사용됩니다.
먼저 JavaScript부터 확장 관계를 보면 이해가 쉽습니다.
JavaScript
├── .js
└── .jsx (React)
TypeScript
├── .ts
└── .tsx (React)
즉,
- .js → 일반 JavaScript
- .jsx → React(JavaScript + JSX)
- .ts → 일반 TypeScript
- .tsx → React(TypeScript + JSX)
번외 2 : AI Studio는 프로토타입을 빠르게 만드는 도구로 생각하는 것이 좋습니다.
AI Studio는 프로토타입을 빠르게 만드는 도구로 생각하는 것이 좋습니다.
AI Studio
↓
초기 개발
↓
GitHub
↓
VSCode
↓
Vercel
또는
Cloud Run
이렇게 하면
- Git으로 버전 관리 가능
- 수정 즉시 재배포 가능
- AI Studio의 Publish 상태와 무관하게 운영 가능
'교육' 카테고리의 다른 글
| 20260803_AI(Replit x Claude) 바이브코딩 혁신 마무리 (0) | 2026.08.03 |
|---|---|
| 20260801_AI(Replit x Claude) 바이브코딩 혁신 1일차 (0) | 2026.08.01 |
| 젠킨스 교육 정리 한마당.. (1) | 2026.06.09 |
| cf.젠킨스에서 배치 작업 가능하긴 할듯 (0) | 2026.05.31 |
| 26년 5월 31일 jenkins를 활용한 CI/CD 환경 구성 2일차 (0) | 2026.05.31 |
