/* ===========================================
MODAL — 노션 본문 렌더링
=========================================== */
.cc-pf-modal-body {
margin-top: 36px;
padding-top: 36px;
border-top: 1px solid rgba(245, 245, 240, 0.08);
color: rgba(245, 245, 240, 0.85);
font-size: 15px;
line-height: 1.75;
}
.cc-pf-modal-body-loading,
.cc-pf-modal-body-error {
padding: 24px 0;
text-align: center;
color: rgba(245, 245, 240, 0.4);
font-size: 13px;
}
.cc-pf-modal-body .cc-nb-p {
margin: 0 0 18px;
color: rgba(245, 245, 240, 0.78);
word-break: keep-all;
}
.cc-pf-modal-body .cc-nb-space { height: 14px; }
.cc-pf-modal-body .cc-nb-h1,
.cc-pf-modal-body .cc-nb-h2,
.cc-pf-modal-body .cc-nb-h3 {
font-family: 'Sandoll Rotary', 'Pretendard', sans-serif;
color: #f5f5f0;
letter-spacing: -0.02em;
word-break: keep-all;
font-weight: 400;
}
.cc-pf-modal-body .cc-nb-h1 { font-size: 28px; margin: 40px 0 18px; line-height: 1.25; }
.cc-pf-modal-body .cc-nb-h2 { font-size: 22px; margin: 36px 0 14px; line-height: 1.3; }
.cc-pf-modal-body .cc-nb-h3 { font-size: 18px; margin: 28px 0 12px; line-height: 1.4; }
.cc-pf-modal-body .cc-nb-ul,
.cc-pf-modal-body .cc-nb-ol {
margin: 0 0 18px;
padding-left: 24px;
color: rgba(245, 245, 240, 0.78);
}
.cc-pf-modal-body .cc-nb-ul li,
.cc-pf-modal-body .cc-nb-ol li {
margin-bottom: 8px;
line-height: 1.7;
}
.cc-pf-modal-body .cc-nb-quote {
margin: 24px 0;
padding: 4px 0 4px 20px;
border-left: 3px solid #faaf40;
color: rgba(245, 245, 240, 0.85);
font-style: italic;
}
.cc-pf-modal-body .cc-nb-callout {
display: flex;
gap: 14px;
padding: 18px 22px;
margin: 20px 0;
background: rgba(250, 175, 64, 0.08);
border-radius: 8px;
border-left: 3px solid #faaf40;
}
.cc-pf-modal-body .cc-nb-callout-icon { flex-shrink: 0; font-size: 20px; line-height: 1.6; }
.cc-pf-modal-body .cc-nb-callout-text {
flex: 1;
color: rgba(245, 245, 240, 0.85);
line-height: 1.65;
}
.cc-pf-modal-body .cc-nb-divider {
height: 1px;
background: rgba(245, 245, 240, 0.1);
border: 0;
margin: 40px 0;
}
.cc-pf-modal-body .cc-nb-figure {
margin: 28px 0;
border-radius: 8px;
overflow: hidden;
}
.cc-pf-modal-body .cc-nb-figure img {
display: block;
width: 100%;
height: auto;
}
.cc-pf-modal-body .cc-nb-figure figcaption {
padding: 12px 16px;
background: rgba(255, 255, 255, 0.03);
font-size: 12px;
color: rgba(245, 245, 240, 0.5);
text-align: center;
}
.cc-pf-modal-body .cc-nb-video {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
margin: 28px 0;
border-radius: 8px;
overflow: hidden;
background: #000;
}
.cc-pf-modal-body .cc-nb-video iframe,
.cc-pf-modal-body .cc-nb-video video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
}
.cc-pf-modal-body .cc-nb-bookmark {
display: block;
padding: 14px 18px;
margin: 18px 0;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 6px;
color: #faaf40;
font-size: 13px;
word-break: break-all;
transition: background 0.2s ease;
}
.cc-pf-modal-body .cc-nb-bookmark:hover { background: rgba(255, 255, 255, 0.07); }
.cc-pf-modal-body .cc-nb-inline-code {
padding: 2px 6px;
background: rgba(255, 255, 255, 0.08);
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.92em;
color: #faaf40;
}
.cc-pf-modal-body .cc-nb-code {
padding: 18px 22px;
margin: 18px 0;
background: #0a0a0a;
border-radius: 8px;
overflow-x: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
line-height: 1.6;
color: rgba(245, 245, 240, 0.8);
}
.cc-pf-modal-body a {
color: #faaf40;
text-decoration: underline;
text-underline-offset: 3px;
}
@media (max-width: 700px) {
.cc-pf-modal-body { font-size: 14px; }
.cc-pf-modal-body .cc-nb-h1 { font-size: 22px; }
.cc-pf-modal-body .cc-nb-h2 { font-size: 18px; }
.cc-pf-modal-body .cc-nb-h3 { font-size: 16px; }
}
<style>
@font-face {
font-family: 'Sandoll Rotary';
src: url('<https://cdn.jsdelivr.net/gh/projectnoonnu/[email protected]/WavvePADO-Regular.woff2>') format('woff2');
font-weight: normal;
font-display: swap;
}
/* ===========================================
Portfolio 페이지 — 공통 + Hero + Filter + Grid
=========================================== */
.cc-pf-page,
.cc-pf-page * {
font-family: 'Pretendard', -apple-system, sans-serif;
box-sizing: border-box;
}
.cc-pf-hero-title,
.cc-pf-card-title,
.cc-pf-card-fallback-title,
.cc-pf-modal-title {
font-family: 'Sandoll Rotary', 'Pretendard', sans-serif !important;
}
.cc-pf-page {
position: relative;
width: 100vw;
margin-left: calc(-50vw + 50%);
background: #0f0f0f;
color: #f5f5f0;
overflow: hidden;
}
.cc-pf-page a {
text-decoration: none;
color: inherit;
}
/* ===========================================
HERO
=========================================== */
.cc-pf-hero {
position: relative;
padding: 140px 0 100px;
background: #0f0f0f;
}
.cc-pf-hero-inner {
max-width: 1600px;
margin: 0 auto;
padding: 0 6%;
display: grid;
grid-template-columns: 1fr auto;
gap: 60px;
align-items: end;
}
.cc-pf-hero-left {
max-width: 800px;
}
.cc-pf-hero-eyebrow {
display: inline-block;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.3em;
color: #faaf40;
margin-bottom: 32px;
}
.cc-pf-hero-title {
font-size: clamp(60px, 9vw, 140px);
font-weight: 400;
line-height: 0.92;
letter-spacing: -0.04em;
color: #f5f5f0;
margin: 0 0 32px;
word-break: keep-all;
}
.cc-pf-hero-sub {
font-size: clamp(15px, 1.3vw, 19px);
font-weight: 500;
line-height: 1.6;
color: rgba(245, 245, 240, 0.65);
max-width: 540px;
word-break: keep-all;
margin: 0;
}
.cc-pf-hero-count {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
padding-bottom: 8px;
}
.cc-pf-hero-count-num {
font-family: 'Sandoll Rotary', sans-serif !important;
font-size: clamp(40px, 4vw, 60px);
line-height: 1;
color: #faaf40;
letter-spacing: -0.03em;
}
.cc-pf-hero-count-label {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.25em;
color: rgba(245, 245, 240, 0.5);
text-transform: uppercase;
}
/* ===========================================
FILTER BAR
=========================================== */
.cc-pf-filter {
position: sticky;
top: 0;
z-index: 10;
background: rgba(15, 15, 15, 0.92);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-top: 1px solid rgba(245, 245, 240, 0.08);
border-bottom: 1px solid rgba(245, 245, 240, 0.08);
}
.cc-pf-filter-inner {
max-width: 1600px;
margin: 0 auto;
padding: 24px 6%;
display: flex;
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.cc-pf-filter-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.cc-pf-filter-sub {
padding-top: 12px;
border-top: 1px solid rgba(245, 245, 240, 0.08);
}
.cc-pf-filter-sub:empty {
display: none;
}
.cc-pf-filter-btn.cc-pf-filter-sub-btn {
font-size: 12px;
padding: 6px 14px;
background: rgba(255, 255, 255, 0.04);
border-color: transparent;
}
.cc-pf-filter-btn.cc-pf-filter-sub-btn.is-active {
background: transparent;
border-color: #faaf40;
color: #faaf40;
font-weight: 600;
}
.cc-pf-filter-btn {
font-family: 'Pretendard', sans-serif;
font-size: 13px;
font-weight: 500;
color: rgba(245, 245, 240, 0.6);
background: transparent;
border: 1px solid rgba(245, 245, 240, 0.15);
padding: 9px 20px;
border-radius: 100px;
cursor: pointer;
transition: all 0.25s ease;
letter-spacing: 0.02em;
white-space: nowrap;
}
.cc-pf-filter-btn:hover {
color: #f5f5f0;
border-color: rgba(245, 245, 240, 0.4);
}
.cc-pf-filter-btn.is-active {
background: #faaf40;
border-color: #faaf40;
color: #0f0f0f;
font-weight: 600;
}
.cc-pf-filter-count {
margin-left: auto;
font-size: 12px;
font-weight: 500;
color: rgba(245, 245, 240, 0.4);
letter-spacing: 0.05em;
}
/* ===========================================
GRID
=========================================== */
.cc-pf-grid-section {
padding: 60px 0 140px;
background: #0f0f0f;
}
.cc-pf-grid-inner {
max-width: 1600px;
margin: 0 auto;
padding: 0 6%;
}
.cc-pf-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 16px;
}
/* 균일 그리드 — 깔끔한 3열 (데스크탑) */
.cc-pf-card {
position: relative;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
background: #1a1a1a;
aspect-ratio: 16 / 10;
transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
grid-column: span 4;
}
.cc-pf-card:hover {
transform: translateY(-8px);
}
.cc-pf-card-thumb {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}
.cc-pf-card:hover .cc-pf-card-thumb {
transform: scale(1.06);
}
/* 썸네일 없을 때 — 그라데이션 배경 */
.cc-pf-card-fallback {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding: 28px 28px 88px;
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%);
transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
gap: 16px;
}
.cc-pf-card-fallback-num {
font-family: 'Sandoll Rotary', sans-serif !important;
font-size: 12px;
font-weight: 400;
color: rgba(245, 245, 240, 0.3);
letter-spacing: 0.08em;
}
.cc-pf-card-fallback-title {
font-size: clamp(20px, 1.6vw, 26px);
font-weight: 400;
line-height: 1.25;
color: rgba(245, 245, 240, 0.92);
letter-spacing: -0.02em;
word-break: keep-all;
margin: 0;
}
.cc-pf-card:hover .cc-pf-card-fallback {
transform: scale(1.02);
}
/* 폴백 그라데이션 변주 (12n 패턴) */
.cc-pf-card:nth-child(6n+1) .cc-pf-card-fallback {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
}
.cc-pf-card:nth-child(6n+2) .cc-pf-card-fallback {
background: linear-gradient(135deg, #2a1810 0%, #3d2818 100%);
}
.cc-pf-card:nth-child(6n+3) .cc-pf-card-fallback {
background: linear-gradient(135deg, #1a2a1a 0%, #1d3d28 100%);
}
.cc-pf-card:nth-child(6n+4) .cc-pf-card-fallback {
background: linear-gradient(135deg, #2a1a2a 0%, #3d1d3d 100%);
}
.cc-pf-card:nth-child(6n+5) .cc-pf-card-fallback {
background: linear-gradient(135deg, #2a2a1a 0%, #3d3d18 100%);
}
.cc-pf-card:nth-child(6n+6) .cc-pf-card-fallback {
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%);
}
.cc-pf-card-fallback-num {
font-family: 'Sandoll Rotary', sans-serif !important;
font-size: 13px;
font-weight: 400;
color: rgba(245, 245, 240, 0.3);
letter-spacing: 0.05em;
}
.cc-pf-card-fallback-title {
font-size: clamp(20px, 1.8vw, 32px);
font-weight: 400;
line-height: 1.2;
color: rgba(245, 245, 240, 0.85);
letter-spacing: -0.02em;
word-break: keep-all;
margin: 0;
}
/* 카드 오버레이 + 정보 */
.cc-pf-card-overlay {
position: absolute;
inset: 0;
background: linear-gradient(180deg,
rgba(0, 0, 0, 0) 30%,
rgba(0, 0, 0, 0.5) 70%,
rgba(0, 0, 0, 0.92) 100%);
pointer-events: none;
opacity: 0.85;
transition: opacity 0.4s ease;
}
.cc-pf-card.has-thumb .cc-pf-card-overlay {
opacity: 0.7;
}
.cc-pf-card:hover .cc-pf-card-overlay {
opacity: 0.95;
}
/* 폴백 카드는 오버레이 약하게 */
.cc-pf-card.no-thumb .cc-pf-card-overlay {
opacity: 0;
}
.cc-pf-card-info {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 24px 28px;
z-index: 2;
}
.cc-pf-card-meta {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.cc-pf-card-category {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.2em;
color: #faaf40;
text-transform: uppercase;
}
.cc-pf-card-divider {
width: 3px;
height: 3px;
background: rgba(245, 245, 240, 0.4);
border-radius: 50%;
}
.cc-pf-card-client {
font-size: 11px;
font-weight: 500;
color: rgba(245, 245, 240, 0.65);
letter-spacing: 0.02em;
}
.cc-pf-card-title {
font-size: clamp(17px, 1.4vw, 22px);
font-weight: 400;
line-height: 1.3;
color: #f5f5f0;
letter-spacing: -0.02em;
margin: 0 0 12px;
word-break: keep-all;
}
.cc-pf-card-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
opacity: 0;
max-height: 0;
transform: translateY(8px);
transition: opacity 0.4s ease, max-height 0.4s ease, transform 0.4s ease;
overflow: hidden;
}
.cc-pf-card:hover .cc-pf-card-tags {
opacity: 1;
max-height: 40px;
transform: translateY(0);
}
.cc-pf-card-tag {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.1em;
color: rgba(245, 245, 240, 0.8);
background: rgba(255, 255, 255, 0.1);
padding: 3px 8px;
border-radius: 100px;
text-transform: uppercase;
backdrop-filter: blur(4px);
}
/* Empty / Loading */
.cc-pf-loading {
grid-column: 1 / -1;
text-align: center;
padding: 120px 0;
color: rgba(245, 245, 240, 0.4);
font-size: 14px;
}
.cc-pf-empty {
grid-column: 1 / -1;
text-align: center;
padding: 100px 0;
color: rgba(245, 245, 240, 0.4);
font-size: 14px;
}
/* ===========================================
MODAL — 작품 상세 보기
=========================================== */
.cc-pf-modal {
position: fixed;
inset: 0;
z-index: 9999;
display: none;
align-items: center;
justify-content: center;
padding: 40px 24px;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
opacity: 0;
transition: opacity 0.3s ease;
overflow-y: auto;
}
.cc-pf-modal.is-open {
display: flex;
opacity: 1;
}
.cc-pf-modal-inner {
position: relative;
width: 100%;
max-width: 1200px;
max-height: calc(100vh - 80px);
background: #1a1a1a;
border-radius: 16px;
overflow-y: auto;
overflow-x: hidden;
transform: scale(0.96) translateY(20px);
transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
scrollbar-width: thin;
scrollbar-color: rgba(245,245,240,0.2) transparent;
}
.cc-pf-modal-inner::-webkit-scrollbar {
width: 8px;
}
.cc-pf-modal-inner::-webkit-scrollbar-thumb {
background: rgba(245,245,240,0.2);
border-radius: 4px;
}
.cc-pf-modal.is-open .cc-pf-modal-inner {
transform: scale(1) translateY(0);
}
.cc-pf-modal-close {
position: absolute;
top: 20px;
right: 20px;
width: 44px;
height: 44px;
background: rgba(0, 0, 0, 0.5);
border: none;
border-radius: 50%;
color: #f5f5f0;
font-size: 20px;
cursor: pointer;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.25s ease, transform 0.25s ease;
backdrop-filter: blur(8px);
}
.cc-pf-modal-close:hover {
background: rgba(250, 175, 64, 0.9);
color: #0f0f0f;
transform: rotate(90deg);
}
/* 비디오/이미지 영역 */
.cc-pf-modal-media {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
background: #000;
overflow: hidden;
}
.cc-pf-modal-media iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
}
.cc-pf-modal-media-image {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
}
.cc-pf-modal-media-fallback {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
}
.cc-pf-modal-media-fallback-icon {
width: 64px;
height: 64px;
border: 2px solid rgba(245, 245, 240, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: rgba(245, 245, 240, 0.5);
}
.cc-pf-modal-media-fallback-text {
font-size: 13px;
font-weight: 500;
color: rgba(245, 245, 240, 0.5);
letter-spacing: 0.05em;
}
/* 작품 정보 */
.cc-pf-modal-content {
padding: 48px 56px 56px;
}
.cc-pf-modal-meta {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.cc-pf-modal-category {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.2em;
color: #faaf40;
text-transform: uppercase;
}
.cc-pf-modal-meta-divider {
width: 3px;
height: 3px;
background: rgba(245, 245, 240, 0.3);
border-radius: 50%;
}
.cc-pf-modal-meta-item {
font-size: 12px;
font-weight: 500;
color: rgba(245, 245, 240, 0.6);
}
.cc-pf-modal-title {
font-size: clamp(28px, 3vw, 44px);
font-weight: 400;
line-height: 1.2;
color: #f5f5f0;
letter-spacing: -0.03em;
margin: 0 0 20px;
word-break: keep-all;
}
.cc-pf-modal-desc {
font-size: 15px;
line-height: 1.7;
color: rgba(245, 245, 240, 0.75);
margin: 0 0 32px;
max-width: 720px;
word-break: keep-all;
}
.cc-pf-modal-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 36px;
}
.cc-pf-modal-tag {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.12em;
color: rgba(245, 245, 240, 0.8);
background: rgba(255, 255, 255, 0.08);
padding: 5px 12px;
border-radius: 100px;
text-transform: uppercase;
}
.cc-pf-modal-link {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
color: #faaf40;
padding: 12px 24px;
border: 1px solid rgba(250, 175, 64, 0.4);
border-radius: 100px;
transition: all 0.25s ease;
letter-spacing: 0.02em;
}
.cc-pf-modal-link:hover {
background: #faaf40;
color: #0f0f0f;
border-color: #faaf40;
}
/* body scroll lock 시 적용 */
body.cc-pf-modal-open {
overflow: hidden;
}
/* ===========================================
반응형
=========================================== */
@media (max-width: 1100px) {
.cc-pf-grid {
gap: 12px;
}
/* 태블릿 — 6열 단순화 */
.cc-pf-card,
.cc-pf-card:nth-child(n) {
grid-column: span 6;
aspect-ratio: 16 / 10;
}
}
@media (max-width: 700px) {
/* HERO */
.cc-pf-hero {
padding: 80px 0 60px;
}
.cc-pf-hero-inner {
grid-template-columns: 1fr;
gap: 32px;
padding: 0 24px;
align-items: start;
}
.cc-pf-hero-eyebrow {
margin-bottom: 20px;
}
.cc-pf-hero-title {
font-size: clamp(44px, 14vw, 80px);
margin-bottom: 24px;
}
.cc-pf-hero-sub {
font-size: 14px;
}
.cc-pf-hero-count {
flex-direction: row;
align-items: baseline;
gap: 12px;
}
.cc-pf-hero-count-num {
font-size: 32px;
}
.cc-pf-hero-count-label {
font-size: 10px;
}
/* FILTER */
.cc-pf-filter-inner {
padding: 16px 24px;
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.cc-pf-filter-inner::-webkit-scrollbar {
display: none;
}
.cc-pf-filter-btn {
font-size: 12px;
padding: 7px 16px;
flex-shrink: 0;
}
.cc-pf-filter-count {
display: none;
}
/* GRID — 모바일은 1열 */
.cc-pf-grid-section {
padding: 40px 0 80px;
}
.cc-pf-grid-inner {
padding: 0 24px;
}
.cc-pf-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.cc-pf-card,
.cc-pf-card:nth-child(n) {
grid-column: span 1;
aspect-ratio: 16 / 10;
}
.cc-pf-card:hover {
transform: none;
}
.cc-pf-card:hover .cc-pf-card-thumb {
transform: none;
}
.cc-pf-card-info {
padding: 18px 20px;
}
.cc-pf-card-fallback {
padding: 20px 22px;
}
/* 모바일에서 카드 정보 + 태그 항상 표시 */
.cc-pf-card .cc-pf-card-tags {
opacity: 1;
max-height: 40px;
transform: translateY(0);
}
/* MODAL */
.cc-pf-modal {
padding: 80px 12px 16px;
align-items: flex-start;
}
.cc-pf-modal-inner {
max-height: calc(100vh - 96px);
border-radius: 12px;
}
.cc-pf-modal-close {
width: 40px;
height: 40px;
top: 10px;
right: 10px;
font-size: 18px;
background: rgba(0, 0, 0, 0.7);
}
.cc-pf-modal-content {
padding: 28px 24px 32px;
}
.cc-pf-modal-title {
font-size: 24px;
}
.cc-pf-modal-desc {
font-size: 14px;
margin-bottom: 24px;
}
}
</style>
<section class="cc-pf-page">
<!-- HERO -->
<div class="cc-pf-hero">
<div class="cc-pf-hero-inner">
<div class="cc-pf-hero-left">
<span class="cc-pf-hero-eyebrow">PORTFOLIO</span>
<h1 class="cc-pf-hero-title">
우리가<br>
만든 것들
</h1>
<p class="cc-pf-hero-sub">
기획부터 제작·운영·관리까지.<br>
문화콘텐츠가 클라이언트와 함께 만들어 온 이야기들입니다.
</p>
</div>
<div class="cc-pf-hero-count">
<span class="cc-pf-hero-count-num" id="cc-pf-count">—</span>
<span class="cc-pf-hero-count-label">Total Works</span>
</div>
</div>
</div>
<!-- FILTER -->
<div class="cc-pf-filter-inner">
<div class="cc-pf-filter-row" id="cc-pf-filters-main">
<button class="cc-pf-filter-btn is-active" data-category="all">전체</button>
<button class="cc-pf-filter-btn" data-category="영상">영상</button>
<button class="cc-pf-filter-btn" data-category="디자인">디자인</button>
<button class="cc-pf-filter-btn" data-category="기획·운영">기획·운영</button>
<span class="cc-pf-filter-count" id="cc-pf-filter-count"></span>
</div>
<div class="cc-pf-filter-row cc-pf-filter-sub" id="cc-pf-filters-sub"></div>
</div>
<!-- GRID -->
<div class="cc-pf-grid-section">
<div class="cc-pf-grid-inner">
<div class="cc-pf-grid" id="cc-pf-grid">
<div class="cc-pf-loading">작품을 불러오는 중…</div>
</div>
</div>
</div>
</section>
<script>
(function() {
var API_BASE = '<https://cc-notion-proxy.master-cba.workers.dev>';
var API_URL = API_BASE + '/portfolio-all';
var DETAIL_URL = API_BASE + '/portfolio-detail';
var allItems = [];
var currentCategory = 'all';
var currentSub = 'all';
var SUB_BY_CATEGORY = {
'영상': ['인터뷰', '홍보', '스케치/하이라이트', '제품', '브랜딩', 'VJ', '기타'],
'디자인': ['웹', '인쇄'],
'기획·운영': []
};
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function escapeAttr(str) { return escapeHtml(str); }
function parseVideoUrl(url) {
if (!url) return null;
var ytMatch = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|v\/)|youtu\.be\/)([^&?\/\s]+)/);
if (ytMatch) return { type: 'youtube', embedUrl: '<https://www.youtube.com/embed/>' + ytMatch[1] + '?autoplay=0&rel=0' };
var vimeoMatch = url.match(/vimeo\.com\/(?:video\/)?(\d+)/);
if (vimeoMatch) return { type: 'vimeo', embedUrl: '<https://player.vimeo.com/video/>' + vimeoMatch[1] + '?autoplay=0' };
return null;
}
// ===== Card =====
function buildCard(item, index) {
var hasThumb = !!item.thumbnail;
var thumbStyle = hasThumb ? 'background-image: url(\'' + item.thumbnail.replace(/'/g, "\\'") + '\');' : '';
var metaParts = [];
if (item.category) metaParts.push('<span class="cc-pf-card-category">' + escapeHtml(item.category) + '</span>');
if (item.client) {
if (metaParts.length > 0) metaParts.push('<span class="cc-pf-card-divider"></span>');
metaParts.push('<span class="cc-pf-card-client">' + escapeHtml(item.client) + '</span>');
}
var subTagHtml = item.subcategory ? '<span class="cc-pf-card-tag">' + escapeHtml(item.subcategory) + '</span>' : '';
var cardContent;
if (hasThumb) {
cardContent = '<div class="cc-pf-card-thumb" style="' + thumbStyle + '"></div>'
+ '<div class="cc-pf-card-overlay"></div>'
+ '<div class="cc-pf-card-info">'
+ ' <div class="cc-pf-card-meta">' + metaParts.join('') + '</div>'
+ ' <h3 class="cc-pf-card-title">' + escapeHtml(item.title) + '</h3>'
+ ' <div class="cc-pf-card-tags">' + subTagHtml + '</div>'
+ '</div>';
} else {
var numStr = String(index + 1).padStart(2, '0');
cardContent = '<div class="cc-pf-card-fallback">'
+ ' <span class="cc-pf-card-fallback-num">— ' + numStr + '</span>'
+ ' <h3 class="cc-pf-card-fallback-title">' + escapeHtml(item.title) + '</h3>'
+ '</div>'
+ '<div class="cc-pf-card-info">'
+ ' <div class="cc-pf-card-meta">' + metaParts.join('') + '</div>'
+ ' <div class="cc-pf-card-tags">' + subTagHtml + '</div>'
+ '</div>';
}
return '<div class="cc-pf-card ' + (hasThumb ? 'has-thumb' : 'no-thumb') + '" data-index="' + index + '">' + cardContent + '</div>';
}
function getFilteredItems() {
return allItems.filter(function(item) {
if (currentCategory !== 'all' && item.category !== currentCategory) return false;
if (currentSub !== 'all' && item.subcategory !== currentSub) return false;
return true;
});
}
function renderGrid() {
var grid = document.getElementById('cc-pf-grid');
if (!grid) return;
var filteredItems = getFilteredItems();
if (filteredItems.length === 0) {
grid.innerHTML = '<div class="cc-pf-empty">해당하는 작품이 없습니다.</div>';
updateFilterCount(0);
return;
}
grid.innerHTML = filteredItems.map(buildCard).join('');
updateFilterCount(filteredItems.length);
grid.querySelectorAll('.cc-pf-card').forEach(function(card) {
card.addEventListener('click', function() {
var idx = parseInt(card.getAttribute('data-index'), 10);
var item = filteredItems[idx];
if (item) openModal(item);
});
});
}
function updateFilterCount(count) {
var el = document.getElementById('cc-pf-filter-count');
if (el) el.textContent = count + ' / ' + allItems.length;
}
function renderSubFilters() {
var container = document.getElementById('cc-pf-filters-sub');
if (!container) return;
if (currentCategory === 'all') { container.innerHTML = ''; return; }
var subs = SUB_BY_CATEGORY[currentCategory] || [];
if (subs.length === 0) { container.innerHTML = ''; return; }
var existingSubs = {};
allItems.forEach(function(item) {
if (item.category === currentCategory && item.subcategory) existingSubs[item.subcategory] = true;
});
var html = '<button class="cc-pf-filter-btn cc-pf-filter-sub-btn ' + (currentSub === 'all' ? 'is-active' : '') + '" data-sub="all">전체</button>';
subs.forEach(function(sub) {
if (!existingSubs[sub]) return;
html += '<button class="cc-pf-filter-btn cc-pf-filter-sub-btn ' + (currentSub === sub ? 'is-active' : '') + '" data-sub="' + escapeAttr(sub) + '">' + escapeHtml(sub) + '</button>';
});
container.innerHTML = html;
container.querySelectorAll('.cc-pf-filter-sub-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
currentSub = btn.getAttribute('data-sub');
renderSubFilters();
renderGrid();
});
});
}
function setupMainFilters() {
var btns = document.querySelectorAll('#cc-pf-filters-main .cc-pf-filter-btn');
btns.forEach(function(btn) {
btn.addEventListener('click', function() {
currentCategory = btn.getAttribute('data-category');
currentSub = 'all';
btns.forEach(function(b) { b.classList.remove('is-active'); });
btn.classList.add('is-active');
renderSubFilters();
renderGrid();
});
});
}
// ===== Modal =====
function buildModal(item) {
var videoData = parseVideoUrl(item.videoUrl);
var mediaHtml;
if (videoData) {
mediaHtml = '<iframe src="' + escapeAttr(videoData.embedUrl) + '" '
+ 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" '
+ 'allowfullscreen></iframe>';
} else if (item.thumbnail) {
mediaHtml = '<div class="cc-pf-modal-media-image" style="background-image: url(\'' + item.thumbnail.replace(/'/g, "\\'") + '\');"></div>';
} else {
mediaHtml = '<div class="cc-pf-modal-media-fallback">'
+ ' <div class="cc-pf-modal-media-fallback-icon">▶</div>'
+ ' <div class="cc-pf-modal-media-fallback-text">VIDEO COMING SOON</div>'
+ '</div>';
}
var metaParts = [];
if (item.category) metaParts.push('<span class="cc-pf-modal-category">' + escapeHtml(item.category) + '</span>');
if (item.subcategory) {
if (metaParts.length > 0) metaParts.push('<span class="cc-pf-modal-meta-divider"></span>');
metaParts.push('<span class="cc-pf-modal-meta-item">' + escapeHtml(item.subcategory) + '</span>');
}
if (item.year) {
if (metaParts.length > 0) metaParts.push('<span class="cc-pf-modal-meta-divider"></span>');
metaParts.push('<span class="cc-pf-modal-meta-item">' + escapeHtml(String(item.year)) + '</span>');
}
if (item.client) {
if (metaParts.length > 0) metaParts.push('<span class="cc-pf-modal-meta-divider"></span>');
metaParts.push('<span class="cc-pf-modal-meta-item">' + escapeHtml(item.client) + '</span>');
}
var descHtml = item.description ? '<p class="cc-pf-modal-desc">' + escapeHtml(item.description) + '</p>' : '';
return '<div class="cc-pf-modal-inner">'
+ ' <button class="cc-pf-modal-close" aria-label="닫기">✕</button>'
+ ' <div class="cc-pf-modal-media">' + mediaHtml + '</div>'
+ ' <div class="cc-pf-modal-content">'
+ ' <div class="cc-pf-modal-meta">' + metaParts.join('') + '</div>'
+ ' <h2 class="cc-pf-modal-title">' + escapeHtml(item.title) + '</h2>'
+ ' ' + descHtml
+ ' <div class="cc-pf-modal-body" id="cc-pf-modal-body">'
+ ' <div class="cc-pf-modal-body-loading">콘텐츠를 불러오는 중...</div>'
+ ' </div>'
+ ' </div>'
+ '</div>';
}
function loadModalBody(pageId) {
if (!pageId) return;
fetch(DETAIL_URL + '?id=' + encodeURIComponent(pageId))
.then(function(res) { return res.json(); })
.then(function(data) {
var el = document.getElementById('cc-pf-modal-body');
if (!el) return;
if (data && data.html && data.html.trim()) {
el.innerHTML = data.html;
} else {
el.style.display = 'none';
}
})
.catch(function(err) {
console.error('Detail load error:', err);
var el = document.getElementById('cc-pf-modal-body');
if (el) el.innerHTML = '<div class="cc-pf-modal-body-error">콘텐츠를 불러올 수 없습니다.</div>';
});
}
function openModal(item) {
var modal = document.getElementById('cc-pf-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'cc-pf-modal';
modal.className = 'cc-pf-modal';
document.body.appendChild(modal);
}
modal.innerHTML = buildModal(item);
requestAnimationFrame(function() {
modal.classList.add('is-open');
document.body.classList.add('cc-pf-modal-open');
});
var closeBtn = modal.querySelector('.cc-pf-modal-close');
if (closeBtn) closeBtn.addEventListener('click', closeModal);
modal.addEventListener('click', function(e) {
if (e.target === modal) closeModal();
});
if (item.id) loadModalBody(item.id);
}
function closeModal() {
var modal = document.getElementById('cc-pf-modal');
if (!modal) return;
modal.classList.remove('is-open');
document.body.classList.remove('cc-pf-modal-open');
setTimeout(function() {
if (!modal.classList.contains('is-open')) modal.innerHTML = '';
}, 300);
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeModal();
});
// ===== Load =====
function load() {
var countEl = document.getElementById('cc-pf-count');
fetch(API_URL)
.then(function(res) { return res.json(); })
.then(function(data) {
allItems = (data && data.items) || [];
if (countEl) countEl.textContent = allItems.length;
setupMainFilters();
renderSubFilters();
renderGrid();
})
.catch(function(err) {
console.error('Portfolio load error:', err);
var grid = document.getElementById('cc-pf-grid');
if (grid) grid.innerHTML = '<div class="cc-pf-empty">작품을 불러올 수 없습니다.</div>';
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', load);
} else {
load();
}
})();
</script>