Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c510296
fix: ํšŒ์›๊ฐ€์ž…๊ณผ ๊ฐœ์ธ์ •๋ณด ์ˆ˜์ • ๋‹‰๋„ค์ž„ ๊ธฐ์ค€ ์ผ์น˜
wkdjh Aug 13, 2025
71a862d
Merge branch 'fix/s3-modal' of https://github.com/CLD-3rd/Final-Team3โ€ฆ
wkdjh Aug 13, 2025
8cff8c3
fix: ๋กœ๊ทธ์•„์›ƒ์‹œ ๋ฉ”์ธํŽ˜์ด์ง€๋กœ ์ด๋™
sinascode Aug 13, 2025
3ea55db
Revert "Develop"
alex052525 Aug 14, 2025
cb33332
Merge pull request #46 from CLD-3rd/revert-45-develop
alex052525 Aug 14, 2025
94d1629
feat: ๋ชจ์ง‘๊ธ€ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ์—์„œ ์กฐํšŒ์ˆ˜ ์ถ”๊ฐ€
sinascode Aug 14, 2025
76cacf5
feat: ์ƒ์„ธ์œ„์น˜๋กœ ์ง€์—ญ ์ž๋™ ์„ค์ •
sinascode Aug 14, 2025
c55c0bc
feat: ์ƒ์„ธํŽ˜์ด์ง€ ๋ชจ์ง‘๊ธ€ ์ทจ์†Œ ๋กœ์ง
sinascode Aug 16, 2025
97b5e7a
feat: ์‹ ์ฒญ๋ชฉ๋ก์— ์ทจ์†Œ๋ฒ„ํŠผ ์ถ”๊ฐ€
sinascode Aug 16, 2025
2d69ca2
Merge pull request #49 from CLD-3rd/feat/participation-cancle
sinascode Aug 16, 2025
5aa38d8
fix: ๊ฑฐ์ ˆ๋œ ๋ชจ์ง‘๊ธ€์€ ์‹ ์ฒญ ์ทจ์†Œ ๋ถˆ๊ฐ€
sinascode Aug 17, 2025
767c3f5
Merge branch 'fix/s3-modal' into develop
alex052525 Aug 17, 2025
b64e128
Merge pull request #50 from CLD-3rd/develop
alex052525 Aug 17, 2025
6b80831
Revert "fix: ํŽ˜์ด์ง€๋„ค์ด์…˜ + ๋ชจ๋‹ฌ ํ•ด๊ฒฐ ์™„๋ฃŒ"
alex052525 Aug 17, 2025
7e91d25
Merge pull request #51 from CLD-3rd/revert-50-develop
alex052525 Aug 17, 2025
4ac3744
Merge branch 'test404' into fix/s3-modal
alex052525 Aug 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 123 additions & 66 deletions app/create-post/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,100 @@ const Toast = ({ message, type, onClose }: { message: string; type: 'success' |
</div>
)

// ์ง€์—ญ ๋งคํ•‘ ํ•จ์ˆ˜
const getRegionFromAddress = (address: string): string => {
const addressLower = address.toLowerCase();

// 1๋‹จ๊ณ„: ๋„/๊ด‘์—ญ์‹œ๋ช…์ด ์ง์ ‘ ํฌํ•จ๋œ ๊ฒฝ์šฐ ์šฐ์„  ๋งค์นญ
const primaryMapping = [
{ keywords: ['์„œ์šธํŠน๋ณ„์‹œ', '์„œ์šธ์‹œ', '์„œ์šธ'], value: 'SEOUL' },
{ keywords: ['๊ฒฝ๊ธฐ๋„', '๊ฒฝ๊ธฐ'], value: 'GYEONGGI' },
{ keywords: ['๊ฐ•์›๋„', '๊ฐ•์›ํŠน๋ณ„์ž์น˜๋„', '๊ฐ•์›'], value: 'GANGWON' },
{ keywords: ['๋Œ€์ „๊ด‘์—ญ์‹œ', '๋Œ€์ „์‹œ'], value: 'DAEJEON' },
{ keywords: ['๋Œ€๊ตฌ๊ด‘์—ญ์‹œ', '๋Œ€๊ตฌ์‹œ'], value: 'DAEGU' },
{ keywords: ['์ธ์ฒœ๊ด‘์—ญ์‹œ', '์ธ์ฒœ์‹œ'], value: 'INCHEON' },
{ keywords: ['๊ด‘์ฃผ๊ด‘์—ญ์‹œ', '๊ด‘์ฃผ์‹œ'], value: 'GWANGJU' },
{ keywords: ['์šธ์‚ฐ๊ด‘์—ญ์‹œ', '์šธ์‚ฐ์‹œ'], value: 'ULSAN' },
{ keywords: ['๋ถ€์‚ฐ๊ด‘์—ญ์‹œ', '๋ถ€์‚ฐ์‹œ'], value: 'BUSAN' },
{ keywords: ['์„ธ์ข…ํŠน๋ณ„์ž์น˜์‹œ', '์„ธ์ข…์‹œ'], value: 'SEJONG' },
{ keywords: ['์ถฉ์ฒญ๋‚จ๋„', '์ถฉ๋‚จ'], value: 'CHUNGNAM' },
{ keywords: ['์ถฉ์ฒญ๋ถ๋„', '์ถฉ๋ถ'], value: 'CHUNGBUK' },
{ keywords: ['์ „๋ผ๋ถ๋„', '์ „๋ถ'], value: 'JEONBUK' },
{ keywords: ['์ „๋ผ๋‚จ๋„', '์ „๋‚จ'], value: 'JEONNAM' },
{ keywords: ['๊ฒฝ์ƒ๋ถ๋„', '๊ฒฝ๋ถ'], value: 'GYEONGBUK' },
{ keywords: ['๊ฒฝ์ƒ๋‚จ๋„', '๊ฒฝ๋‚จ'], value: 'GYEONGNAM' },
{ keywords: ['์ œ์ฃผํŠน๋ณ„์ž์น˜๋„', '์ œ์ฃผ๋„'], value: 'JEJU' },
];

// 1๋‹จ๊ณ„ ๋งค์นญ ์‹œ๋„
for (const region of primaryMapping) {
for (const keyword of region.keywords) {
if (addressLower.includes(keyword)) {
return region.value;
}
}
}

// 2๋‹จ๊ณ„: ๊ณ ์œ ํ•œ ์‹œ/๊ตฐ๋ช…์œผ๋กœ ๋งค์นญ (์ค‘๋ณต๋˜์ง€ ์•Š๋Š” ๊ฒƒ๋“ค๋งŒ)
const uniqueCityMapping = [
// ๊ฒฝ๊ธฐ๋„ ๊ณ ์œ  ์‹œ/๊ตฐ
{ keywords: ['์ˆ˜์›์‹œ', '์„ฑ๋‚จ์‹œ', '๊ณ ์–‘์‹œ', '์šฉ์ธ์‹œ', '๋ถ€์ฒœ์‹œ', '์•ˆ์‚ฐ์‹œ', '์•ˆ์–‘์‹œ', '๋‚จ์–‘์ฃผ์‹œ', 'ํ™”์„ฑ์‹œ', 'ํ‰ํƒ์‹œ', '์˜์ •๋ถ€์‹œ', '์‹œํฅ์‹œ', 'ํŒŒ์ฃผ์‹œ', '๊ด‘๋ช…์‹œ', '๊น€ํฌ์‹œ', '๊ตฐํฌ์‹œ', '์ด์ฒœ์‹œ', '์–‘์ฃผ์‹œ', '์˜ค์‚ฐ์‹œ', '๊ตฌ๋ฆฌ์‹œ', '์•ˆ์„ฑ์‹œ', 'ํฌ์ฒœ์‹œ', '์˜์™•์‹œ', 'ํ•˜๋‚จ์‹œ', '์—ฌ์ฃผ์‹œ', '์—ฌ์ฃผ๊ตฐ', '์–‘ํ‰๊ตฐ', '๋™๋‘์ฒœ์‹œ', '๊ณผ์ฒœ์‹œ', '๊ฐ€ํ‰๊ตฐ', '์—ฐ์ฒœ๊ตฐ'], value: 'GYEONGGI' },

// ๊ฐ•์›๋„ ๊ณ ์œ  ์‹œ/๊ตฐ
{ keywords: ['์ถ˜์ฒœ์‹œ', '์›์ฃผ์‹œ', '๊ฐ•๋ฆ‰์‹œ', '๋™ํ•ด์‹œ', 'ํƒœ๋ฐฑ์‹œ', '์†์ดˆ์‹œ', '์‚ผ์ฒ™์‹œ', 'ํ™์ฒœ๊ตฐ', 'ํšก์„ฑ๊ตฐ', '์˜์›”๊ตฐ', 'ํ‰์ฐฝ๊ตฐ', '์ •์„ ๊ตฐ', '์ฒ ์›๊ตฐ', 'ํ™”์ฒœ๊ตฐ', '์–‘๊ตฌ๊ตฐ', '์ธ์ œ๊ตฐ', '๊ณ ์„ฑ๊ตฐ', '์–‘์–‘๊ตฐ'], value: 'GANGWON' },

// ์ถฉ์ฒญ๋‚จ๋„ ๊ณ ์œ  ์‹œ/๊ตฐ
{ keywords: ['์ฒœ์•ˆ์‹œ', '๊ณต์ฃผ์‹œ', '๋ณด๋ น์‹œ', '์•„์‚ฐ์‹œ', '์„œ์‚ฐ์‹œ', '๋…ผ์‚ฐ์‹œ', '๊ณ„๋ฃก์‹œ', '๋‹น์ง„์‹œ', '๊ธˆ์‚ฐ๊ตฐ', '๋ถ€์—ฌ๊ตฐ', '์„œ์ฒœ๊ตฐ', '์ฒญ์–‘๊ตฐ', 'ํ™์„ฑ๊ตฐ', '์˜ˆ์‚ฐ๊ตฐ', 'ํƒœ์•ˆ๊ตฐ'], value: 'CHUNGNAM' },

// ์ถฉ์ฒญ๋ถ๋„ ๊ณ ์œ  ์‹œ/๊ตฐ
{ keywords: ['์ฒญ์ฃผ์‹œ', '์ถฉ์ฃผ์‹œ', '์ œ์ฒœ์‹œ', '๋ณด์€๊ตฐ', '์˜ฅ์ฒœ๊ตฐ', '์˜๋™๊ตฐ', '์ฆํ‰๊ตฐ', '์ง„์ฒœ๊ตฐ', '๊ดด์‚ฐ๊ตฐ', '์Œ์„ฑ๊ตฐ', '๋‹จ์–‘๊ตฐ'], value: 'CHUNGBUK' },

// ์ „๋ผ๋ถ๋„ ๊ณ ์œ  ์‹œ/๊ตฐ
{ keywords: ['์ „์ฃผ์‹œ', '๊ตฐ์‚ฐ์‹œ', '์ต์‚ฐ์‹œ', '์ •์์‹œ', '๋‚จ์›์‹œ', '๊น€์ œ์‹œ', '์™„์ฃผ๊ตฐ', '์ง„์•ˆ๊ตฐ', '๋ฌด์ฃผ๊ตฐ', '์žฅ์ˆ˜๊ตฐ', '์ž„์‹ค๊ตฐ', '์ˆœ์ฐฝ๊ตฐ', '๊ณ ์ฐฝ๊ตฐ', '๋ถ€์•ˆ๊ตฐ'], value: 'JEONBUK' },

// ์ „๋ผ๋‚จ๋„ ๊ณ ์œ  ์‹œ/๊ตฐ
{ keywords: ['๋ชฉํฌ์‹œ', '์—ฌ์ˆ˜์‹œ', '์ˆœ์ฒœ์‹œ', '๋‚˜์ฃผ์‹œ', '๊ด‘์–‘์‹œ', '๋‹ด์–‘๊ตฐ', '๊ณก์„ฑ๊ตฐ', '๊ตฌ๋ก€๊ตฐ', '๊ณ ํฅ๊ตฐ', '๋ณด์„ฑ๊ตฐ', 'ํ™”์ˆœ๊ตฐ', '์žฅํฅ๊ตฐ', '๊ฐ•์ง„๊ตฐ', 'ํ•ด๋‚จ๊ตฐ', '์˜์•”๊ตฐ', '๋ฌด์•ˆ๊ตฐ', 'ํ•จํ‰๊ตฐ', '์˜๊ด‘๊ตฐ', '์žฅ์„ฑ๊ตฐ', '์™„๋„๊ตฐ', '์ง„๋„๊ตฐ', '์‹ ์•ˆ๊ตฐ'], value: 'JEONNAM' },

// ๊ฒฝ์ƒ๋ถ๋„ ๊ณ ์œ  ์‹œ/๊ตฐ
{ keywords: ['ํฌํ•ญ์‹œ', '๊ฒฝ์ฃผ์‹œ', '๊น€์ฒœ์‹œ', '์•ˆ๋™์‹œ', '๊ตฌ๋ฏธ์‹œ', '์˜์ฃผ์‹œ', '์˜์ฒœ์‹œ', '์ƒ์ฃผ์‹œ', '๋ฌธ๊ฒฝ์‹œ', '๊ฒฝ์‚ฐ์‹œ', '๊ตฐ์œ„๊ตฐ', '์˜์„ฑ๊ตฐ', '์ฒญ์†ก๊ตฐ', '์˜์–‘๊ตฐ', '์˜๋•๊ตฐ', '์ฒญ๋„๊ตฐ', '๊ณ ๋ น๊ตฐ', '์„ฑ์ฃผ๊ตฐ', '์น ๊ณก๊ตฐ', '์˜ˆ์ฒœ๊ตฐ', '๋ด‰ํ™”๊ตฐ', '์šธ์ง„๊ตฐ', '์šธ๋ฆ‰๊ตฐ'], value: 'GYEONGBUK' },

// ๊ฒฝ์ƒ๋‚จ๋„ ๊ณ ์œ  ์‹œ/๊ตฐ
{ keywords: ['์ฐฝ์›์‹œ', '์ง„์ฃผ์‹œ', 'ํ†ต์˜์‹œ', '์‚ฌ์ฒœ์‹œ', '๊น€ํ•ด์‹œ', '๋ฐ€์–‘์‹œ', '๊ฑฐ์ œ์‹œ', '์–‘์‚ฐ์‹œ', '์˜๋ น๊ตฐ', 'ํ•จ์•ˆ๊ตฐ', '์ฐฝ๋…•๊ตฐ', '๋‚จํ•ด๊ตฐ', 'ํ•˜๋™๊ตฐ', '์‚ฐ์ฒญ๊ตฐ', 'ํ•จ์–‘๊ตฐ', '๊ฑฐ์ฐฝ๊ตฐ', 'ํ•ฉ์ฒœ๊ตฐ'], value: 'GYEONGNAM' },

// ์ œ์ฃผ๋„ ๊ณ ์œ  ์‹œ
{ keywords: ['์ œ์ฃผ์‹œ', '์„œ๊ท€ํฌ์‹œ'], value: 'JEJU' },
];

// 2๋‹จ๊ณ„ ๋งค์นญ ์‹œ๋„
for (const region of uniqueCityMapping) {
for (const keyword of region.keywords) {
if (addressLower.includes(keyword)) {
return region.value;
}
}
}

// 3๋‹จ๊ณ„: ํŠน๋ณ„ํ•œ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ (๋Œ€์ „, ๋Œ€๊ตฌ, ๊ด‘์ฃผ์˜ ๊ฒฝ์šฐ ์‹œ๋ช…๋งŒ์œผ๋กœ๋„ ๋งค์นญ)
if (addressLower.includes('๋Œ€์ „')) return 'DAEJEON';
if (addressLower.includes('๋Œ€๊ตฌ')) return 'DAEGU';
if (addressLower.includes('๊ด‘์ฃผ')) return 'GWANGJU';
if (addressLower.includes('์šธ์‚ฐ')) return 'ULSAN';
if (addressLower.includes('๋ถ€์‚ฐ')) return 'BUSAN';
if (addressLower.includes('์ธ์ฒœ')) return 'INCHEON';
if (addressLower.includes('์„ธ์ข…')) return 'SEJONG';
if (addressLower.includes('์ œ์ฃผ')) return 'JEJU';

return '';
};

const cleanAddress = (address: string): string => {
return address
.replace(/๋Œ€ํ•œ๋ฏผ๊ตญ\s*/, '')
.replace(/Republic of Korea\s*/, '')
.replace(/South Korea\s*/, '')
.trim();
};

export default function CreatePostPage() {
const router = useRouter()
const [formData, setFormData] = useState({
Expand Down Expand Up @@ -135,7 +229,7 @@ export default function CreatePostPage() {
const locationInputRef = useRef<HTMLInputElement>(null)
const predictionsRef = useRef<HTMLDivElement>(null)
const GOOGLE_PLACES_API_KEY = process.env.NEXT_PUBLIC_GOOGLE_PLACES_API_KEY || ""

// Google Places Autocomplete Service ์ดˆ๊ธฐํ™”
useEffect(() => {
const loadGoogleMapsScript = () => {
Expand All @@ -157,7 +251,6 @@ export default function CreatePostPage() {
loadGoogleMapsScript().catch(console.error)
}, [])

// Google Places Autocomplete Service๋ฅผ ์‚ฌ์šฉํ•œ ์žฅ์†Œ ์˜ˆ์ธก
const fetchPlacePredictions = async (input: string) => {
if (!input.trim() || input.length < 1) {
setPredictions([])
Expand Down Expand Up @@ -219,7 +312,7 @@ export default function CreatePostPage() {
setPredictions([])
setShowPredictions(false)
}
}, 200) // 200ms๋กœ ์ค„์—ฌ์„œ ๋” ๋น ๋ฅธ ๋ฐ˜์‘
}, 200)

return () => clearTimeout(timeoutId)
}, [formData.location])
Expand All @@ -241,10 +334,17 @@ export default function CreatePostPage() {
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])

// ์˜ˆ์ธก ๊ฒฐ๊ณผ ์„ ํƒ ์ฒ˜๋ฆฌ
const handlePredictionSelect = (prediction: PlacePrediction) => {
// ์ฃผ์š” ์žฅ์†Œ๋ช…๋งŒ ์„ค์ • (์ฃผ์†Œ๋Š” ์ œ์™ธ)
setFormData(prev => ({ ...prev, location: prediction.structured_formatting.main_text }))

const cleanedAddress = cleanAddress(prediction.description);
const detectedRegion = getRegionFromAddress(prediction.description);

setFormData(prev => ({
...prev,
location: cleanedAddress,
town: detectedRegion
}));

setShowPredictions(false)
setPredictions([])
}
Expand Down Expand Up @@ -475,65 +575,6 @@ export default function CreatePostPage() {
</div>
</div>

<div className="space-y-3">
<Label className="text-lg font-semibold text-gray-900">
์ง€์—ญ <span className="text-red-500">*</span>
</Label>
<div className="relative">
<Input
placeholder="์ง€์—ญ์„ ์„ ํƒํ•˜์„ธ์š”"
value={townOptions.find(opt => opt.value === formData.town)?.label || ""}
readOnly
className="h-14 text-lg border-2 border-gray-200 rounded-2xl focus:border-black focus:ring-0 bg-gray-50 pr-14 cursor-pointer"
onClick={() => settownModalOpen(true)}
required
/>
<button
type="button"
className="absolute right-4 top-1/2 -translate-y-1/2 p-2 hover:bg-gray-200 rounded-xl transition-colors"
onClick={() => settownModalOpen(true)}
>
<Search className="w-5 h-5 text-gray-500" />
</button>
</div>

{/* ์ง€์—ญ ์„ ํƒ ๋ชจ๋‹ฌ */}
{townModalOpen && (
<div className="fixed inset-0 flex items-center justify-center z-50 bg-black/40 p-4">
<div className="bg-white rounded-3xl p-8 w-full max-w-md shadow-2xl">
<h3 className="text-2xl font-bold text-gray-900 mb-6 text-center">์ง€์—ญ ์„ ํƒ</h3>
<div className="grid grid-cols-3 gap-3 mb-6">
{townOptions.map((option) => (
<button
key={option.value}
type="button"
className={`p-3 rounded-xl border-2 transition-all ${
formData.town === option.value
? "border-black bg-black text-white"
: "border-gray-200 bg-white text-gray-700 hover:border-gray-300"
}`}
onClick={() => {
setFormData(prev => ({ ...prev, town: option.value }))
settownModalOpen(false)
}}
>
{option.label}
</button>
))}
</div>
<button
type="button"
className="w-full py-3 border-2 border-gray-200 rounded-2xl text-gray-700 font-semibold hover:bg-gray-50 transition-colors"
onClick={() => settownModalOpen(false)}
>
๋‹ซ๊ธฐ
</button>
</div>
</div>
)}
</div>

{/* Google Places API ์ž๋™์™„์„ฑ์ด ์ ์šฉ๋œ ์ƒ์„ธ ์œ„์น˜ ์ž…๋ ฅ */}
<div className="space-y-3 relative">
<Label className="text-lg font-semibold text-gray-900">
์ƒ์„ธ ์œ„์น˜ <span className="text-red-500">*</span>
Expand Down Expand Up @@ -598,6 +639,23 @@ export default function CreatePostPage() {
)}
</div>

{/* ์ง€์—ญ ์„ ํƒ (์ž๋™์œผ๋กœ ์„ค์ •๋จ) */}
<div className="space-y-3">
<Label className="text-lg font-semibold text-gray-900">
์ง€์—ญ <span className="text-red-500">*</span>
</Label>
<div className="relative">
<Input
value={townOptions.find(opt => opt.value === formData.town)?.label || ""}
readOnly
className="h-14 text-lg border-2 border-gray-200 rounded-2xl focus:border-black focus:ring-0 bg-gray-100 pr-14 cursor-not-allowed"
required
/>

</div>

</div>

<div className="space-y-4">
<Label className="text-lg font-semibold text-gray-900">
๋‚ ์งœ ๋ฐ ์‹œ๊ฐ„ <span className="text-red-500">*</span>
Expand Down Expand Up @@ -756,7 +814,6 @@ export default function CreatePostPage() {
</div>
</div>
</div>

<div className="pt-8">
<button
type="submit"
Expand Down
Loading
Loading