WallpaperMachine
The wallpapers you love. A new home on your Mac.
기관이 필요한 물품과 수량을 먼저 올리면, 사진 한 장으로 여럿이 나눠 그 목표를 채우는 나눔 서비스
Translation pending
LifestyleTypeScript · CSSGitHub @Estherdami-cmdOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
https://temporary-flying-mistral-kc8mjzk.vercel.app/
Translation pending
Loading the full README. The saved excerpt is shown below.
기관이 "이 물건이 이만큼 필요해요"를 먼저 올리고, 개인이 사진 한 장으로 그 목표를 나눠 채우는 서비스입니다. 게시판에 물건을 올려두고 기관이 일일이 골라가는 방식(당나귀형)의 모니터링 부담과 한 기관이 싹쓸이해가는 허점을, 크라우드펀딩식 구조로 뒤집었습니다. .env.local에 OpenAI API 키를 넣습니다. 키가 없어도 목업으로 동작하지만, 실제 사진 판독은 안 됩니다. http://localhost:3000 에서 열립니다. /beneficiary는 헤더 메뉴의 "기관등록"으로도 들어갈 수 있습니다. 시연할 때 두 번째 창으로 띄워 기관 역할을 맡겨도 됩니다.Read the full README ↗
기관이 "이 물건이 이만큼 필요해요"를 먼저 올리고, 개인이 사진 한 장으로 그 목표를 나눠 채우는 서비스입니다.
게시판에 물건을 올려두고 기관이 일일이 골라가는 방식(당나귀형)의 모니터링 부담과 한 기관이 싹쓸이해가는 허점을, 크라우드펀딩식 구조로 뒤집었습니다.
npm install
.env.local에 OpenAI API 키를 넣습니다. 키가 없어도 목업으로 동작하지만, 실제 사진 판독은 안 됩니다.
OPENAI_API_KEY=발급받은_키
# OPENAI_MODEL=gpt-5.4-mini # 선택. 비우면 이 값을 쓴다
# 선택. 기관 목록을 공공데이터에서 다시 받아올 때만 필요합니다(아래 "기관 데이터" 참고).
# DATA_GO_KR_SERVICE_KEY=발급받은_서비스키
# ADMIN_SYNC_TOKEN=아무_문자열
# 기관 관리 코드. 신청 수락·거절, 연락처 보기, 요청 등록에 필요합니다.
# 로컬에서는 비워두면 코드 없이 열리고, 운영(Vercel)에서는 비워두면 이 기능들이 막힙니다.
# ADMIN_TOKEN=기관들과_공유할_코드
npm run dev
http://localhost:3000 에서 열립니다.
| 탭 | 역할 |
|---|---|
| 진행률 게시판 | 기관들이 올린 요청과 현재 진행률을 한눈에 |
| 기부자 모드 | 사진 등록 → AI 인식 → 매칭 → 신청 (4단계) |
| 기관 관리 | 기관 선택 → 기관 홈 → 요청 등록 또는 신청 확인·수락/거절 |
/beneficiary는 헤더 메뉴의 "기관 관리"로 들어갑니다. 시연할 때 두 번째 창으로 띄워 기관 역할을 맡겨도 됩니다.
| 화면 | 주소 | 하는 일 |
|---|---|---|
| 기관 입장 | /beneficiary | 기관 선택, 관리 코드 확인 |
| 기관 홈 | /beneficiary/[orgId] | 처리할 신청과 요청별 진행률 확인 |
| 요청 올리기 | /beneficiary/[orgId]/needs/new | 필요 물품 등록, 등록 완료 안내 |
| 신청 확인 | /beneficiary/[orgId]/applications/[applicationId] | 물품·연락처 확인, 날짜 골라 수락 또는 거절, 진행률 변화 확인 |
관리 코드는 탭을 닫기 전까지 유지됩니다. 기관별 계정은 없고 코드 하나를 함께 쓰므로, 주소의 기관 id는 권한 경계가 아니라 화면 범위입니다.
OpenAI Vision(gpt-5.4-mini)으로 실제 사진을 판독합니다. 품목명·카테고리·유통기한을 읽어 옵니다. 응답은 JSON 스키마로 강제하고, 카테고리는 앱이 아는 6종 안에서만 고르게 합니다.
사진은 한 장 또는 두 장을 받습니다. 두 장이면 한 번의 요청에 같이 실어 보내고, 각 사진 앞에 [사진 1] 제품 사진 / [사진 2] 유통기한 사진 라벨 텍스트를 끼워 역할을 알려줍니다. 이 라벨이 없으면 모델이 어느 쪽이 유통기한 사진인지 모릅니다. 날짜는 사진 2를 우선해 읽고, 거기서 못 찾으면 사진 1에서 찾습니다.
모델은 아래 데모 사진(라벨 2026.09.04)으로 후보를 여러 번 돌려보고 골랐습니다.
| 모델 | 날짜 정확도 | 응답 시간 |
|---|---|---|
gpt-5.4-mini (effort medium) | 5/5 | 2.1~3.4초 |
gpt-4.1-mini (temperature 0) | 3/3 | 3.5~5.1초 |
gpt-5.4-nano (effort low) | 1/2 — 연도를 틀림 | 2.0~3.0초 |
reasoning.effort를 low로 낮추면 gpt-5.4-mini도 2026을 2028로 읽은 적이 있어 medium으로 고정했습니다. 유통기한 오독은 이 앱에서 가장 비싼 실수라 속도보다 정확도를 삽니다.
화면 1의 예시 사진 불러오기 (2장) 버튼은 저장소에 함께 둔 실제 제품 사진 두 장을 두 슬롯에 한 번에 넣습니다. 발표 중에 파일 탐색기를 열거나 물건을 들고 다닐 필요가 없습니다.
| 파일 | 내용 |
|---|---|
public/demo/juice-front.jpg | 아침에주스 앞면 — 품목명·용량 |
public/demo/juice-back.jpg | 뒷면 — 유통기한 2026.09.04 |
품목명은 앞면에, 유통기한은 뒷면에만 있어서 두 슬롯이 왜 필요한지 그 자체로 보여주는 예시입니다. 뒷면에는 품목보고번호 2006240126도 같이 찍혀 있는데 그걸 날짜로 착각하지 않습니다.
판독 우선순위는 이렇습니다.
milk_expired.jpg → 우유), 없으면 파일 이름+크기 해시(같은 사진이면 항상 같은 결과). 이때는 결과 카드 맨 위에 주황 배너로 *"AI 서버에 연결하지 못했어요 — 아래 내용은 실제 판독 결과가 아니라 예시 데이터예요"*가 뜹니다. 뱃지가 "나눔 가능"을 확신에 차서 보여주는데 그 근거가 가짜 데이터라, 뱃지보다 위에서 먼저 읽히게 뒀습니다.사진에 물품이 없으면 목업으로 덮지 않고 422와 함께 "물품이 잘 보이게 다시 찍어주세요"를 돌려줍니다. 엉뚱한 품목을 확신에 차서 보여주는 게 더 나쁘기 때문입니다.
나눔 가능 여부는 모델이 정하지 않습니다. 모델이 읽은 유통기한을 받아 서버의 evaluateShareable가 판정합니다.
다른 모델·다른 벤더로 바꾸려면 app/api/recognize/route.ts 한 파일만 고치면 됩니다. 응답 형태(itemName, category, expiryDate, shareable, reason)만 맞추면 나머지는 그대로 동작합니다.
사진 2장을 gpt-5.4-mini(effort medium)로 판독한 실제 사용량입니다.
| 항목 | 값 |
|---|---|
| 입력 토큰 | 3,917 (사진 2장 + 프롬프트) |
| 출력 토큰 | 464 (추론 382 포함) |
| 1회 비용 | $0.005 (약 7원) |
돈은 병목이 아닙니다. 한도가 병목입니다. 무료 등급은 모델마다 하루 50회(RPD)로
묶여 있고, 24h ÷ 50 = 28.8분마다 1회분씩 차는 leaky bucket이라 실질 시간당 2회입니다.
크레딧 잔액이 남아 있어도 이 제한은 그대로입니다 — 등급은 잔액이 아니라 실제 결제한
누적 금액으로 올라갑니다(프로모션 크레딧은 자격에 안 잡힙니다).
한도에 걸리면 앱은 목업으로 떨어지고 화면이 "오늘 AI 판독 한도를 다 썼어요"라고 말합니다. 리허설은 데모 모드로 하면 호출이 아예 안 나갑니다.
AI로 확인하기
→ 실제 냉동 생지 봉지 사진에서 치즈볼생지 750g과 소비기한 2027-03-09을 읽어내고
초록 나눔 가능 뱃지가 뜸성인용 기저귀로 다시 인식 → 다음이 날짜로 수락 (진행률 +10) 클릭 → 결과 화면에서 30% → 50% 확인진행 현황 보기로 진행률 게시판에 가면 **50%**로 올라 있음요청·기부·신청·기관은 Firestore에 있습니다(lib/firebase.ts, lib/store.ts). 팀원 전체가 같은
DB를 보므로 각자 실행해도 데이터가 공유됩니다. 컬렉션이 비어 있으면 시드 데이터로 한 번 채웁니다.
신청 화면(/apply)의 전달 장소에 기관 주소와 지도를 보여줍니다. 좌표는 공공데이터에서
받아온 값을 그대로 씁니다(lib/data/pohang-orgs.json).
지도는 두 겹입니다.
JavaScript 키는 DeliveryMap.tsx에 코드로 박아뒀습니다. lib/firebase.ts의 Firebase 웹
설정과 같은 판단입니다 — 브라우저에 실려 나가는 공개 값이고, 숨겨도 개발자도구를 열면
보입니다. 환경변수로 뒀을 때 Vercel에 변수가 없어서 배포에서 조용히 폴백되는 문제도
있었습니다.
진짜 보안은 카카오 콘솔의 사이트 도메인 제한입니다. 그래서 이것만 등록해두면 됩니다.
카카오 개발자 콘솔 → 내 애플리케이션 → 앱 설정 → 플랫폼 → Web → 사이트 도메인
http://localhost:3000
https://sharing-khaki.vercel.app
등록되지 않은 도메인에서는 SDK가 401을 주고 OSM으로 떨어집니다. 응답 메시지가
domain mismatched! caller=... 라서 원인을 바로 알 수 있고, 브라우저 콘솔에도
[DeliveryMap] 카카오 지도 실패 — 가 찍힙니다.
REST API 키와 네이티브 앱 키는 성격이 다릅니다. 그건 비밀 값이니 코드나 저장소에 넣지 마세요. 이 기능에 필요한 건 JavaScript 키뿐입니다.
지도 아래 "카카오맵으로 길찾기"는 도착지가 채워진 상태로 길찾기를 엽니다. 이 링크는 키가 필요 없습니다. 모바일에서는 카카오가 applink로 넘겨 "앱으로 열기 / 설치 없이 웹으로 보기"를 사용자가 고릅니다.
무료급식소 같은 기관은 지도 서비스에 장소로 등록돼 있지 않습니다(검색해도 안 나옵니다). 그래서 장소 ID가 아니라 이름과 좌표를 직접 넘깁니다 — 이러면 등록되지 않은 장소도 이름표 붙은 핀으로 뜹니다.
네이버 지도 길찾기는 쓰지 않습니다. 모바일 브라우저에서 URL이 통합검색으로 리다이렉트되어 도착지가 전달되지 않았습니다(실측).
기부하려는 물품과 가장 비슷한 요청을 위로 올립니다. 정렬 키는 세 단계입니다
(lib/store.ts의 matchNeeds):
1순위 품목 일치도 exact → similar → different
2순위 필요도 점수 부족분·긴급도·거리로 계산
3순위 거리
품목 일치도가 점수보다 우선입니다. 그래서 20점짜리 "찹쌀"(같은 분류)이 33점짜리 "스팸"(다른 분류)보다 위에 옵니다 — 점수만으로 정렬하면 분류가 다른데 아주 급한 요청이 딱 맞는 요청보다 위로 오게 됩니다.
두 겹입니다. 위가 실패하면 아래로 떨어집니다.
genericName을 같이 뽑습니다
("백미 5kg" → "쌀", "햇반 210g" → "즉석밥"). 기관이 요청을 등록할 때도
한 번 뽑습니다. 매칭 경로에서는 모델을 부르지 않습니다 — 화면을 열 때마다
API를 쓰게 되고, 순위가 매번 달라지면 설명할 수 없습니다.lib/rules.ts의 canonicalItemName, isSameItem).포함 관계는 짧은 쪽이 두 글자 이상일 때만 인정합니다. 한 글자를 허용하면
"찹쌀"이 "쌀"을 포함해 같은 물건이 되고, 두 글자까지 막으면 "우유"와 "흰 우유"를
놓칩니다. 이 경계는 npm run check:items가 지킵니다.
npm run check:items # 품목 매칭 회귀 검사 (러너 없이 node로 돈다)
# 아직 비어 있는 요청만 채움 (품목명 종류가 몇 개든 호출은 1회)
curl "http://localhost:3000/api/admin/backfill-generic-names"
# 전부 다시 계산 — 문자열 규칙으로 들어간 값을 AI 값으로 갈아끼울 때
curl "http://localhost:3000/api/admin/backfill-generic-names?force=1"
키가 없거나 한도에 걸리면 문자열 규칙 값으로 채워집니다. 매칭이 멈추지는 않고 정확도만 낮아집니다.
기부를 받는 기관 114곳은 지어낸 데이터가 아니라 공공데이터포털에서 받아온 실제 기관입니다.
출처는 두 곳이고, 서비스 소개 페이지(/service) 에 밝혀 둡니다.
| 출처 | 기관 | 수 | 좌표 |
|---|---|---|---|
| 경상북도_푸드뱅크 현황 | 푸드뱅크·푸드마켓 | 5 | 주소를 Nominatim으로 지오코딩 |
| 포항시_생활 지도 시설 현황 | 지역아동센터 63 · 요양원 34 · 무료급식소 12 | 109 | 원본에 포함 |
두 출처는 서로를 대체하지 않습니다. 푸드뱅크는 기부를 받아 나눠주는 거점이고, 나머지는 물품이
실제로 가 닿는 수혜 기관입니다. Beneficiary.source로 구분하며, 한쪽을 갱신할 때 다른 쪽을 지우지 않습니다.
생활지도 API에는 카테고리 필터가 없습니다(ctgry_nm 등을 넘겨도 무시되고 항상 전체가 옵니다).
109곳을 얻으려면 64,426건을 1,000건씩 65번 받아 직접 걸러야 하고, 한 번에 2분쯤 걸립니다.
게다가 원본이 2023-08-28 수집 스냅샷이라 자주 불러도 내용이 같습니다. 그래서
lib/data/pohang-orgs.json에 스냅샷을 커밋해 두고 Firestore로 서비스합니다.
# 공공데이터를 다시 받아 Firestore에 반영 (2분 소요, 서비스키 필요)
curl "http://localhost:3000/api/admin/sync-orgs"
# 커밋된 스냅샷으로만 반영 (즉시, 서비스키 불필요)
curl "http://localhost:3000/api/admin/sync-orgs?source=snapshot"
# 받아온 결과만 확인 (Firestore 안 건드림) — 스냅샷 파일을 이걸로 만듭니다
curl "http://localhost:3000/api/admin/sync-orgs?dryRun=1" > lib/data/pohang-orgs.json
# 기관이 늘어난 뒤 기존 요청을 새 기관에도 골고루 나누기
curl "http://localhost:3000/api/admin/sync-orgs?source=snapshot&redistribute=1"
운영 환경에서는 ?token=$ADMIN_SYNC_TOKEN이 필요합니다.
두 원본 모두 기관 운영시간·연락처가 없습니다. 실존 기관 이름 옆에 지어낸 운영시간을 붙이면 화면에서 사실로 읽히므로, 비워 두고 "운영일은 신청 후 협의"·"수거 시간은 미확인"으로 표시합니다. AI 추천 날짜도 기관 운영일 제약을 빼고 기부자가 가능한 요일만으로 계산합니다.
app/
page.tsx 3개 탭 전환
_components/
NeedBoard.tsx 진행률 게시판
DonorFlow.tsx 기부자 4단계 (핵심 화면)
OrgEntry.tsx 기관 입장
OrgHome.tsx 기관 홈
NeedForm.tsx 요청 올리기
ApplicationReview.tsx 신청 확인·처리
AdminCode.tsx 관리 코드 입력·확인 (공용)
NeedProgress.tsx 진행률 바 (공용)
api/
recognize/ AI 인식 (OpenAI Vision + 목업 폴백)
needs/ 기관 요청 등록·조회
donations/ 기부 물품
match/[id]/ 필요도·거리 기반 추천
slots/[id]/ 양쪽 가능 날짜 계산
applications/ 신청·수락·거절
ui.ts 공용 스타일
lib/
rules.ts 순수 규칙 (유통기한 판정, 거리, 샘플 데이터)
store.ts Firestore 저장소 + 추천 로직
Next.js 15 (App Router) · React 19 · TypeScript. 외부 의존성 없음.