Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 7 additions & 7 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Coming - Jpop 아티스트 내한 공연 정보</title>
<meta name="description" content="Jpop 아티스트 내한 공연 정보를 한 곳에서 확인하세요. 공연 일정, 아티스트, 발매 소식을 통합 제공하는 Coming입니다." />
<title>커밍</title>
<meta name="description" content="Jpop 아티스트 내한 공연 정보를 한 곳에서 확인하세요. 공연 일정, 아티스트, 발매 소식을 통합 제공하는 커밍입니다." />
<meta name="naver-site-verification" content="f1b23619755e41df4ceaf3f69bdde9197f455143" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Coming" />
<meta property="og:title" content="Coming - Jpop 아티스트 내한 공연 정보" />
<meta property="og:description" content="Jpop 아티스트 내한 공연 정보를 한 곳에서 확인하세요. 공연 일정, 아티스트, 발매 소식을 통합 제공하는 Coming입니다." />
<meta property="og:site_name" content="커밍" />
<meta property="og:title" content="커밍 - Jpop 아티스트 내한 공연 정보" />
<meta property="og:description" content="Jpop 아티스트 내한 공연 정보를 한 곳에서 확인하세요. 공연 일정, 아티스트, 발매 소식을 통합 제공하는 커밍입니다." />
<meta property="og:image" content="https://comingg.com/logo-transparent.png" />
<meta property="og:url" content="https://comingg.com" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Coming - Jpop 아티스트 내한 공연 정보" />
<meta name="twitter:description" content="Jpop 아티스트 내한 공연 정보를 한 곳에서 확인하세요. 공연 일정, 아티스트, 발매 소식을 통합 제공하는 Coming입니다." />
<meta name="twitter:title" content="커밍 - Jpop 아티스트 내한 공연 정보" />
<meta name="twitter:description" content="Jpop 아티스트 내한 공연 정보를 한 곳에서 확인하세요. 공연 일정, 아티스트, 발매 소식을 통합 제공하는 커밍입니다." />
<meta name="twitter:image" content="https://comingg.com/logo-transparent.png" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
Expand Down
7 changes: 0 additions & 7 deletions public/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,36 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://comingg.com/</loc>
<lastmod>2026-08-10</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://comingg.com/artists</loc>
<lastmod>2026-08-10</lastmod>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://comingg.com/concerts</loc>
<lastmod>2026-08-10</lastmod>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://comingg.com/releases</loc>
<lastmod>2026-08-10</lastmod>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://comingg.com/calendar</loc>
<lastmod>2026-08-10</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://comingg.com/terms</loc>
<lastmod>2026-08-10</lastmod>
<changefreq>yearly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://comingg.com/privacy</loc>
<lastmod>2026-08-10</lastmod>
<changefreq>yearly</changefreq>
<priority>0.2</priority>
</url>
Expand Down
4 changes: 4 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/react'

import useAuthStore, { SESSION_HINT } from '@/stores/authStore'
import { buildWebsiteJsonLd, toSafeJsonLd } from '@/utils/structuredData'
import AdminLayout from '@/components/layout/AdminLayout'
import AdminRoute from '@/components/layout/AdminRoute'
import Layout from '@/components/layout/Layout'
Expand Down Expand Up @@ -46,6 +47,8 @@ function App() {
}, [clearUser])

return (
<>
<script type="application/ld+json">{toSafeJsonLd(buildWebsiteJsonLd())}</script>
<Layout>
<ScrollToTop />
<Routes>
Expand Down Expand Up @@ -87,6 +90,7 @@ function App() {
<Analytics />
<SpeedInsights />
</Layout>
</>
)
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/Footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ function Footer() {
return (
<footer className={styles.footer}>
<div className={styles.inner}>
<span className={styles.copy}>© {new Date().getFullYear()} Coming</span>
<span className={styles.copy}>© {new Date().getFullYear()} 커밍</span>
<nav className={styles.links} aria-label="정책 링크">
<Link to={ROUTES.TERMS} className={styles.link}>서비스 이용약관</Link>
<Link to={ROUTES.PRIVACY} className={styles.link}>개인정보처리방침</Link>
Expand Down
6 changes: 3 additions & 3 deletions src/constants/policy.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
export const TERMS_CONTENT = `시행일: 2026년 7월 1일

제1조 (목적)
이 약관은 Coming(이하 "서비스")의 이용과 관련하여 운영자와 이용자 간의 권리, 의무 및 책임 사항을 규정함을 목적으로 합니다.
이 약관은 커밍(이하 "서비스")의 이용과 관련하여 운영자와 이용자 간의 권리, 의무 및 책임 사항을 규정함을 목적으로 합니다.

제2조 (정의)
1. "서비스"란 운영자가 제공하는 Jpop 아티스트 내한 공연 정보 통합 웹 플랫폼 Coming 및 관련 제반 서비스를 의미합니다.
1. "서비스"란 운영자가 제공하는 Jpop 아티스트 내한 공연 정보 통합 웹 플랫폼 커밍 및 관련 제반 서비스를 의미합니다.
2. "이용자"란 이 약관에 동의하고 서비스를 이용하는 자를 말합니다.
3. "회원"이란 소셜 로그인을 통해 가입하여 서비스를 이용하는 자를 말합니다.

Expand Down Expand Up @@ -54,7 +54,7 @@ export const TERMS_CONTENT = `시행일: 2026년 7월 1일

export const PRIVACY_CONTENT = `시행일: 2026년 7월 1일

Coming(이하 "서비스")은 개인정보보호법 제30조에 따라 이용자의 개인정보를 보호하고 관련 고충을 신속하게 처리하기 위해 다음과 같이 개인정보처리방침을 수립·공개합니다.
커밍(이하 "서비스")은 개인정보보호법 제30조에 따라 이용자의 개인정보를 보호하고 관련 고충을 신속하게 처리하기 위해 다음과 같이 개인정보처리방침을 수립·공개합니다.

제1조 (수집하는 개인정보 항목 및 수집 방법)

Expand Down
33 changes: 33 additions & 0 deletions src/hooks/usePageMeta.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useEffect } from 'react'

const DEFAULT_TITLE = '커밍'
const DEFAULT_OG_TITLE = '커밍 - Jpop 아티스트 내한 공연 정보'
const DEFAULT_DESCRIPTION = 'Jpop 아티스트 내한 공연 정보를 한 곳에서 확인하세요. 공연 일정, 아티스트, 발매 소식을 통합 제공하는 커밍입니다.'
const DEFAULT_URL = 'https://comingg.com'
const DEFAULT_IMAGE = 'https://comingg.com/logo-transparent.png'

function setMetaContent(property, content) {
document.querySelector(`meta[property="${property}"]`)?.setAttribute('content', content)
}

// SSR/프리렌더링이 없는 CSR 환경이라 JS를 실행하지 않는 공유 미리보기 봇(카카오톡·페이스북 등)에는
// 반영되지 않는다. 브라우저 탭 제목과 JS를 렌더링하는 검색엔진(Googlebot 등) 대상 개선용.
export default function usePageMeta({ title, description, path, image }) {
useEffect(() => {
if (!title) return

document.title = title
setMetaContent('og:title', title)
setMetaContent('og:description', description || DEFAULT_DESCRIPTION)
setMetaContent('og:url', `https://comingg.com${path}`)
setMetaContent('og:image', image || DEFAULT_IMAGE)

return () => {
document.title = DEFAULT_TITLE
setMetaContent('og:title', DEFAULT_OG_TITLE)
setMetaContent('og:description', DEFAULT_DESCRIPTION)
setMetaContent('og:url', DEFAULT_URL)
setMetaContent('og:image', DEFAULT_IMAGE)
}
}, [title, description, path, image])
}
2 changes: 1 addition & 1 deletion src/hooks/usePageTitle.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { useEffect } from 'react'
export default function usePageTitle(title) {
useEffect(() => {
document.title = title
return () => { document.title = 'Coming' }
return () => { document.title = '커밍' }
}, [title])
}
13 changes: 8 additions & 5 deletions src/pages/ArtistDetailPage.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState } from 'react'
import { Link, useParams, useSearchParams } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

Expand All @@ -14,6 +14,7 @@ import SourceCredit from '@/components/ui/SourceCredit'
import SpotifyIcon from '@/components/ui/SpotifyIcon'
import XIcon from '@/components/ui/XIcon'
import YouTubeIcon from '@/components/ui/YouTubeIcon'
import usePageMeta from '@/hooks/usePageMeta'
import { getArtist, getArtistConcerts, getArtistReleases, followArtist, unfollowArtist } from '@/services/artistApi'
import useAuthStore from '@/stores/authStore'
import useLoginModalStore from '@/stores/loginModalStore'
Expand Down Expand Up @@ -100,10 +101,12 @@ function ArtistDetailPage() {
retry: false,
})

useEffect(() => {
if (artist?.name) document.title = `${artist.name} — Coming`
return () => { document.title = 'Coming' }
}, [artist?.name])
usePageMeta({
title: artist?.name ? `${artist.name} — 커밍` : undefined,
description: artist?.name ? `${artist.name} 아티스트 프로필 및 내한 공연 정보` : undefined,
path: `/artists/${artistId}`,
image: artist?.imageUrl,
})

const { data: concertsData, isLoading: concertsLoading } = useQuery({
queryKey: ['artist-concerts', artistId, concertTab, concertPage],
Expand Down
2 changes: 1 addition & 1 deletion src/pages/ArtistsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function ArtistsPage() {
const openLoginModal = useLoginModalStore((s) => s.open)
const effectiveFollowedOnly = followedOnly && !!user

usePageTitle('아티스트 — Coming')
usePageTitle('아티스트 — 커밍')

useEffect(() => {
if (inputValue === urlQueryRef.current) return
Expand Down
2 changes: 1 addition & 1 deletion src/pages/CalendarPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function CalendarPage() {
const [selectedDate, setSelectedDate] = useState(null)
const [viewMode, setViewMode] = useState('all') // 'all' | 'my'

usePageTitle('캘린더 — Coming')
usePageTitle('캘린더 — 커밍')

const user = useAuthStore((s) => s.user)
const isLoggedIn = !!user
Expand Down
13 changes: 9 additions & 4 deletions src/pages/ConcertDetailPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import BackButton from '@/components/ui/BackButton'
import Badge from '@/components/ui/Badge'
import EmptyState from '@/components/ui/EmptyState'
import InquiryModal from '@/components/ui/InquiryModal'
import usePageMeta from '@/hooks/usePageMeta'
import { getConcert, getConcertSetlist } from '@/services/concertApi'
import { addToCalendar, removeFromCalendar } from '@/services/calendarApi'
import useAuthStore from '@/stores/authStore'
Expand Down Expand Up @@ -49,10 +50,14 @@ function ConcertDetailPage() {
retry: false,
})

useEffect(() => {
if (concert?.title) document.title = `${concert.title} — Coming`
return () => { document.title = 'Coming' }
}, [concert?.title])
usePageMeta({
title: concert?.title ? `${concert.title} — 커밍` : undefined,
description: concert
? `${(concert.artists ?? []).map((a) => a.name).join(' · ')} · ${concert.venue} · ${formatDate(concert.startDate)}`
: undefined,
path: `/concerts/${concertId}`,
image: concert?.posterUrl,
})

const { data: setlistData } = useQuery({
queryKey: ['concert-setlist', concertId],
Expand Down
2 changes: 1 addition & 1 deletion src/pages/ConcertsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function ConcertsPage() {
const openLoginModal = useLoginModalStore((s) => s.open)
const effectiveFollowedOnly = followedOnly && !!user

usePageTitle('공연 — Coming')
usePageTitle('공연 — 커밍')

useEffect(() => {
if (inputValue === urlQueryRef.current) return
Expand Down
2 changes: 2 additions & 0 deletions src/pages/HomePage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 4px 0;
margin: -4px 0;
overflow-y: auto;
scrollbar-width: none;
}
Expand Down
4 changes: 2 additions & 2 deletions src/pages/MyPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,8 @@ function MyPage() {
const [inquiryPage, setInquiryPage] = useState(1)

useEffect(() => {
document.title = '마이페이지 — Coming'
return () => { document.title = 'Coming' }
document.title = '마이페이지 — 커밍'
return () => { document.title = '커밍' }
}, [])

const { data: user } = useQuery({
Expand Down
13 changes: 8 additions & 5 deletions src/pages/ReleaseDetailPage.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useState, useEffect } from 'react'
import { useState } from 'react'
import { Link, useParams, useNavigate, useLocation } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'

import ArtistAliasName from '@/components/artist/ArtistAliasName'
import EmptyState from '@/components/ui/EmptyState'
import SourceCredit from '@/components/ui/SourceCredit'
import SpotifyIcon from '@/components/ui/SpotifyIcon'
import usePageMeta from '@/hooks/usePageMeta'
import { getRelease } from '@/services/releaseApi'
import { ROUTES } from '@/constants/routes'
import { getArtistColor } from '@/utils/artistColor'
Expand Down Expand Up @@ -36,10 +37,12 @@ function ReleaseDetailPage() {
retry: false,
})

useEffect(() => {
if (release?.title) document.title = `${release.title} — Coming`
return () => { document.title = 'Coming' }
}, [release?.title])
usePageMeta({
title: release?.title ? `${release.title} — 커밍` : undefined,
description: release?.title ? `${release.artistName} · ${release.title} 발매 정보` : undefined,
path: `/releases/${releaseId}`,
image: release?.coverUrl,
})

function handleBack() {
locationKey !== 'default' ? navigate(-1) : navigate(ROUTES.RELEASES)
Expand Down
2 changes: 1 addition & 1 deletion src/pages/ReleasesPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function ReleasesPage() {
const openLoginModal = useLoginModalStore((s) => s.open)
const effectiveFollowedOnly = followedOnly && !!user

usePageTitle('음악 — Coming')
usePageTitle('음악 — 커밍')

useEffect(() => {
if (inputValue === urlQueryRef.current) return
Expand Down
6 changes: 3 additions & 3 deletions src/pages/SignupPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ function SignupPage() {
const nicknameTimer = useRef(null)

useEffect(() => {
document.title = '회원가입 — Coming'
return () => { document.title = 'Coming' }
document.title = '회원가입 — 커밍'
return () => { document.title = '커밍' }
}, [])

function handleAllAgreed(checked) {
Expand Down Expand Up @@ -199,7 +199,7 @@ function SignupPage() {
<Logo size="lg" />
</div>
<h1 className={styles.title}>회원가입</h1>
<p className={styles.subtitle}>Coming을 이용하려면 아래 정보를 입력해주세요.</p>
<p className={styles.subtitle}>커밍을 이용하려면 아래 정보를 입력해주세요.</p>

<form className={styles.form} onSubmit={handleSubmit} noValidate>
{/* 닉네임 */}
Expand Down
12 changes: 12 additions & 0 deletions src/utils/structuredData.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ export function buildConcertEventJsonLd(concert) {
}
}

// 사이트 전역용 schema.org/WebSite 구조화 데이터. 브랜드 키워드("커밍") 검색 시 사이트 정체성 인식용.
export function buildWebsiteJsonLd() {
return {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: '커밍',
alternateName: 'Coming',
url: 'https://comingg.com',
description: 'Jpop 아티스트 내한 공연 정보를 한 곳에서 확인하세요. 공연 일정, 아티스트, 발매 소식을 통합 제공하는 커밍입니다.',
}
}

// script 태그 조기 종료(</script>) 인젝션 방지
export function toSafeJsonLd(data) {
return JSON.stringify(data).replace(/</g, '\\u003c')
Expand Down