Skip to content

Commit 6293be6

Browse files
committed
Merge remote-tracking branch 'origin/contents'
# Conflicts: # src/content/posts/2026-03-28-macau.md
2 parents 4c94843 + e5faa8c commit 6293be6

13 files changed

Lines changed: 1604 additions & 76 deletions

.agent/rules/node-version.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# commit-map 프로젝트 룰
2+
3+
## Node.js 버전
4+
5+
이 프로젝트는 Node.js 20 이상이 필요합니다. npm 명령어 실행 전 항상:
6+
7+
```bash
8+
nvm use 20
9+
```
10+
11+
또는 shell에서:
12+
```bash
13+
source ~/.nvm/nvm.sh && nvm use 20
14+
```

.agent/workflows/add-content-field.md

Lines changed: 233 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,61 +4,205 @@ description: Astro Content Collection에 새 필드 추가하는 방법
44

55
# Astro Content Collection 필드 추가 가이드
66

7+
> ⚠️ **중요 규칙**:
8+
> 1. 장소(Location)를 추가할 때 **반드시 구글맵에서 검색**하여 정확한 위도/경도를 입력하세요! 임의로 좌표를 추측하지 마세요.
9+
> 2. 장소명은 **한글을 먼저** 쓰고, 괄호 안에 일본어/영어 원어를 넣으세요. 예: `"가와구치코 로프웨이 (河口湖ロープウェイ)"`
10+
711
Location 데이터나 포스트에 새 필드를 추가할 때 수정해야 하는 파일들:
812

13+
## 수정해야 하는 파일 요약
14+
15+
| 순서 | 파일 | 설명 |
16+
|------|------|------|
17+
| 1 | `src/content.config.ts` | Zod 스키마에 필드 추가 |
18+
| 2 | `src/components/map/providers/types.ts` | TypeScript 인터페이스에 필드 추가 |
19+
| 3 | `src/pages/posts/[slug].astro` | 데이터 매핑에 필드 추가 |
20+
| 4 | `src/components/map/MapContainer.tsx` | (선택) 지도 Popup에서 렌더링 |
21+
22+
---
23+
924
## 1. Content Schema 수정 (필수!)
1025

11-
**`src/content.config.ts`** - 이 파일이 실제 Astro content collection 설정 파일입니다.
26+
**파일:** `src/content.config.ts`
27+
28+
> ⚠️ **주의**: `src/content/config.ts`가 아니라 `src/content.config.ts`입니다!
29+
30+
### Location에 필드 추가
1231

1332
```typescript
1433
// locationSchema에 새 필드 추가
1534
const locationSchema = z.object({
1635
name: z.string(),
1736
lat: z.number(),
1837
lng: z.number(),
19-
// ... 기존 필드들
20-
link: z.string().optional(), // 새로 추가한 필드
21-
visitDate: z.string().optional(), // 새로 추가한 필드
38+
type: locationTypeSchema, // 장소 타입 enum
39+
order: z.number().optional(), // 순서
40+
note: z.string().optional(), // 간단한 메모
41+
link: z.string().optional(), // 외부 링크 (빈 문자열이면 Google Maps 자동 생성)
42+
visitDate: z.string().optional(), // 방문 날짜 (예: "10/1")
43+
contents: z.array(contentSectionSchema).optional(), // 상세 콘텐츠 배열
44+
images: z.array(z.object({ // 이미지 배열
45+
src: z.string(),
46+
alt: z.string().optional(),
47+
caption: z.string().optional(),
48+
})).optional(),
49+
country: z.string().optional(), // 국가 (필터링용)
2250
});
2351
```
2452

25-
> ⚠️ **주의**: `src/content/config.ts`가 아니라 `src/content.config.ts`입니다!
53+
### 새로운 장소 타입(enum) 추가
54+
55+
```typescript
56+
// locationTypeSchema에 새 타입 추가
57+
const locationTypeSchema = z.enum([
58+
'attraction', 'hotel', 'restaurant', 'cafe', 'transport', 'airport',
59+
'shopping', 'nature', 'temple', 'museum', 'zoo', 'theater', 'market',
60+
'beach', 'mountain', 'viewpoint', 'bar', 'palace', 'spa', 'gym', 'church',
61+
// 여기에 새 타입 추가: 'newType',
62+
]).optional();
63+
```
64+
65+
### 포스트 자체에 필드 추가
66+
67+
```typescript
68+
const postsCollection = defineCollection({
69+
type: 'content',
70+
schema: z.object({
71+
title: z.string(),
72+
date: z.coerce.date(),
73+
endDate: z.coerce.date().optional(), // 여행 종료일
74+
locations: z.array(locationSchema),
75+
country: z.string(), // 나라 (필수)
76+
countries: z.array(z.string()).optional(), // 여러 나라 경유 시
77+
tripType: z.array(tripTypeSchema).optional(),
78+
tags: z.array(z.string()).optional(),
79+
thumbnail: z.string().optional(),
80+
excerpt: z.string().optional(),
81+
draft: z.boolean().optional(), // 숨김 처리 여부
82+
}),
83+
});
84+
```
85+
86+
---
2687

2788
## 2. TypeScript 타입 수정
2889

29-
**`src/components/map/providers/types.ts`** - Location 인터페이스에 필드 추가
90+
**파일:** `src/components/map/providers/types.ts`
91+
92+
### Location 인터페이스에 필드 추가
3093

3194
```typescript
3295
export interface Location {
33-
// ... 기존 필드들
34-
link?: string;
35-
visitDate?: string;
96+
lat: number;
97+
lng: number;
98+
name: string;
99+
slug?: string;
100+
link?: string; // 외부 링크
101+
visitDate?: string; // 방문 날짜
102+
order?: number;
103+
note?: string;
104+
type?: string;
105+
date?: Date;
106+
excerpt?: string;
107+
country?: string;
108+
// 여기에 새 필드 추가
36109
}
37110
```
38111

112+
### 새 장소 타입의 아이콘 추가
113+
114+
```typescript
115+
export const locationTypeIcons: Record<string, string> = {
116+
attraction: '🏛️',
117+
hotel: '🏨',
118+
restaurant: '🍽️',
119+
// ...기존 타입들...
120+
church: '',
121+
// 여기에 새 타입 아이콘 추가: newType: '🆕',
122+
};
123+
```
124+
125+
---
126+
39127
## 3. 페이지에서 데이터 전달
40128

41-
**`src/pages/posts/[slug].astro`** - 두 군데 수정:
129+
**파일:** `src/pages/posts/[slug].astro`
130+
131+
### 두 군데 매핑 수정 필요
42132

43133
```typescript
44-
// locations 매핑에 새 필드 추가
134+
// 1️⃣ locations 매핑 (전체 데이터 - line 21~32)
45135
const locations = post.data.locations.map((loc, index) => ({
46-
// ... 기존 필드들
136+
lat: loc.lat,
137+
lng: loc.lng,
138+
name: loc.name,
139+
order: loc.order ?? index + 1,
140+
note: loc.note,
141+
type: loc.type,
47142
link: loc.link,
48143
visitDate: loc.visitDate,
144+
contents: loc.contents,
145+
images: loc.images,
146+
// 여기에 새 필드 추가
49147
}));
50148

51-
// mapLocations에도 추가 (MapContainer로 전달)
149+
// 2️⃣ mapLocations 매핑 (지도 컴포넌트용 - line 34~37)
52150
const mapLocations = locations.map(l => ({
53-
// ... 기존 필드들
54-
link: l.link,
55-
visitDate: l.visitDate,
151+
lat: l.lat, lng: l.lng, name: l.name, order: l.order, note: l.note, type: l.type,
152+
link: l.link, visitDate: l.visitDate,
153+
// 여기에 새 필드 추가 (지도에서 필요한 경우)
56154
}));
57155
```
58156

59-
## 4. 컴포넌트에서 렌더링
157+
### 페이지에서 렌더링
158+
159+
```astro
160+
<!-- link 필드 활용 예시 (line 109~119) -->
161+
<!-- 빈 문자열("")이면 Google Maps 자동 생성, 값이 있으면 해당 링크 사용 -->
162+
{loc.link !== undefined ? (
163+
<a href={loc.link === ''
164+
? `https://www.google.com/maps/search/?api=1&query=${loc.lat},${loc.lng}`
165+
: loc.link}
166+
target="_blank" rel="noopener noreferrer" class="location-link">
167+
{loc.name}
168+
<span class="link-icon">↗</span>
169+
</a>
170+
) : (
171+
loc.name
172+
)}
173+
174+
<!-- images 필드 활용 예시 (line 125~134) -->
175+
{loc.images && loc.images.length > 0 && (
176+
<div class="location-images">
177+
{loc.images.map(img => (
178+
<figure class="image-figure">
179+
<img src={img.src} alt={img.alt || loc.name} loading="lazy" />
180+
{img.caption && <figcaption>{img.caption}</figcaption>}
181+
</figure>
182+
))}
183+
</div>
184+
)}
185+
186+
<!-- contents 필드 활용 예시 (line 136~145) -->
187+
{loc.contents && loc.contents.length > 0 && (
188+
<div class="location-contents">
189+
{loc.contents.map(content => (
190+
<div class="content-section">
191+
{content.heading && <h4>{content.heading}</h4>}
192+
<p>{content.text}</p>
193+
</div>
194+
))}
195+
</div>
196+
)}
197+
```
198+
199+
---
200+
201+
## 4. 컴포넌트에서 렌더링 (선택)
202+
203+
**파일:** `src/components/map/MapContainer.tsx`
60204

61-
**`src/components/map/MapContainer.tsx`** - Popup에서 새 필드 표시
205+
지도 Popup에서 새 필드를 표시하려면:
62206

63207
```tsx
64208
<Popup>
@@ -69,22 +213,83 @@ const mapLocations = locations.map(l => ({
69213
</Popup>
70214
```
71215

216+
---
217+
72218
## 5. 마크다운 파일에서 사용
73219

74-
**`src/content/posts/*.md`**
220+
**파일:** `src/content/posts/*.md`
221+
222+
### 완전한 Location 예시
75223

76224
```yaml
77225
locations:
78-
- name: "장소명"
79-
lat: 35.123
80-
lng: 127.456
81-
link: "https://maps.app.goo.gl/xxx"
82-
visitDate: "10/1"
226+
- name: "콜로세움"
227+
lat: 41.8902
228+
lng: 12.4922
229+
type: attraction
230+
order: 1
231+
note: "로마 제국의 대표적인 건축물"
232+
link: "https://maps.app.goo.gl/xxx" # 또는 빈 문자열 ""로 자동 생성
233+
visitDate: "2/10"
234+
contents:
235+
- heading: "💡 팁"
236+
text: "오전 일찍 가면 줄이 짧아요"
237+
- heading: "📸 포토 스팟"
238+
text: "3층에서 전체 전경을 찍을 수 있어요"
239+
images:
240+
- src: "/images/rome/colosseum.jpg"
241+
alt: "콜로세움 전경"
242+
caption: "해질녘의 콜로세움"
83243
```
84244
85-
## 트러블슈팅
245+
### link 필드 사용 패턴
246+
247+
| 값 | 동작 |
248+
|---|---|
249+
| 생략 | 장소명만 텍스트로 표시 (링크 없음) |
250+
| `""` (빈 문자열) | 자동으로 Google Maps 링크 생성 (`lat,lng` 기반) |
251+
| `"https://..."` | 해당 URL로 링크 |
252+
253+
---
254+
255+
## 6. visitDate 기반 그룹핑 기능
256+
257+
`[slug].astro`에서 자동으로 `visitDate`별로 장소를 그룹핑합니다:
258+
259+
```typescript
260+
// visitDate별 그룹핑 (line 52~60)
261+
const groupedByDate = locations.reduce((acc, loc) => {
262+
const date = loc.visitDate || '날짜 미정';
263+
if (!acc[date]) acc[date] = [];
264+
acc[date].push(loc);
265+
return acc;
266+
}, {} as Record<string, typeof locations>);
267+
```
268+
269+
이 기능을 활용하면 여행 일정별로 장소가 자동 그룹됩니다.
270+
271+
---
272+
273+
## 7. 트러블슈팅
274+
275+
### 스키마 Validation 에러 (Invalid enum value)
276+
277+
**에러 예시:**
278+
```
279+
locations.1.type: Invalid enum value. Expected 'attraction' | ... received 'church'
280+
```
281+
282+
**해결:**
283+
1. `src/content.config.ts`의 `locationTypeSchema`에 새 타입 추가
284+
2. `src/components/map/providers/types.ts`의 `locationTypeIcons`에 아이콘 추가
285+
286+
### 데이터가 undefined로 나올 때
86287
87-
데이터가 undefined로 나오면:
88288
1. `src/content.config.ts` 스키마에 필드가 있는지 확인
89-
2. dev 서버 재시작: `npm run dev`
90-
3. 캐시 삭제 후 재시작: `rm -rf .astro && npm run dev`
289+
2. `src/pages/posts/[slug].astro`의 locations 매핑에 필드를 추가했는지 확인
290+
3. dev 서버 재시작: `npm run dev`
291+
4. 캐시 삭제 후 재시작: `rm -rf .astro && npm run dev`
292+
293+
### 지도에 데이터가 안 보일 때
294+
295+
`mapLocations` 매핑에도 해당 필드를 추가했는지 확인하세요.

astro.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import react from '@astrojs/react';
55
// https://astro.build/config
66
export default defineConfig({
77
integrations: [react()],
8-
site: 'https://gongdongho12.github.io',
8+
site: 'https://map.dongholab.com',
99
// base path 제거 - 루트에서 서빙
1010
output: 'static',
1111
});

public/CNAME

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
map.dongholab.com

src/components/map/providers/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const locationTypeIcons: Record<string, string> = {
5454
palace: '🏰',
5555
spa: '💆',
5656
gym: '🏋️',
57+
church: '⛪',
5758
};
5859

5960
// 특별 스타일 적용할 타입 (호텔, 음식 관련)

src/content.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const tripTypeSchema = z.enum([
1818
const locationTypeSchema = z.enum([
1919
'attraction', 'hotel', 'restaurant', 'cafe', 'transport', 'airport',
2020
'shopping', 'nature', 'temple', 'museum', 'zoo', 'theater', 'market',
21-
'beach', 'mountain', 'viewpoint', 'bar', 'palace', 'spa', 'gym',
21+
'beach', 'mountain', 'viewpoint', 'bar', 'palace', 'spa', 'gym', 'church',
2222
]).optional();
2323

2424
// 장소별 콘텐츠 섹션

0 commit comments

Comments
 (0)