diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 700b103..8ebd3bd 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -8,7 +8,8 @@ "Bash(npm run build:*)", "Bash(python3:*)", "mcp__figma__get_figma_data", - "mcp__figma__download_figma_images" + "mcp__figma__download_figma_images", + "Bash(npx tsc:*)" ] } } diff --git a/index.css b/index.css index cece0cd..0bad73e 100644 --- a/index.css +++ b/index.css @@ -20,11 +20,11 @@ --color-purple-glow: rgba(166, 130, 255, 0.2); --color-purple-80: rgba(166, 130, 255, 0.8); - /* Background Colors */ - --color-bg-dark: #121a1d; - --color-bg-darker: #0d1416; - --color-bg-card: #1c2a2e; - --color-bg-card-inner: #2a3a3e; + /* Background Colors - Teal-600 based */ + --color-bg-dark: #002224; + --color-bg-darker: #001a1c; + --color-bg-card: #003538; + --color-bg-card-inner: #004548; /* Text Colors */ --color-text-white: #ffffff; @@ -97,7 +97,7 @@ height: 100%; padding: var(--spacing-page); overflow: hidden; - background-color: #0d1416; + background-color: #002224; position: relative; } @@ -136,7 +136,7 @@ height: 100%; display: flex; flex-direction: column; - background-color: #0d1416; + background-color: #002224; overflow-y: auto; } @@ -7452,7 +7452,7 @@ display: flex; align-items: center; justify-content: center; - background-color: #0d1416; + background-color: #002224; padding: 1rem; } @@ -7560,7 +7560,7 @@ transition: all 0.2s ease; border: none; background-color: #94FBE0; - color: #0d1416; + color: #002224; } .social-connect-button:hover { @@ -7576,3 +7576,1166 @@ .social-connect-button.secondary:hover { background-color: rgba(255, 255, 255, 0.1); } + +/* ============================================ + My Info Page (내 정보) + ============================================ */ + +.myinfo-page { + flex: 1; + padding: 2rem; + max-width: 900px; + background-color: #002224; +} + +.myinfo-title { + font-size: 1.75rem; + font-weight: 700; + color: #FFFFFF; + margin-bottom: 1.5rem; +} + +/* 탭 네비게이션 */ +.myinfo-tabs { + display: flex; + gap: 0.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 2rem; +} + +.myinfo-tab { + padding: 0.75rem 1rem; + background: none; + border: none; + color: rgba(255, 255, 255, 0.5); + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + position: relative; + transition: color 0.2s ease; +} + +.myinfo-tab:hover { + color: rgba(255, 255, 255, 0.8); +} + +.myinfo-tab.active { + color: #FFFFFF; +} + +.myinfo-tab.active::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, #a6ffea, #a682ff); +} + +/* 탭 컨텐츠 */ +.myinfo-content { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.myinfo-section { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.myinfo-section-title { + font-size: 0.875rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.6); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.myinfo-placeholder { + color: rgba(255, 255, 255, 0.5); + font-size: 0.9rem; + padding: 2rem; + text-align: center; + background: rgba(255, 255, 255, 0.03); + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.06); +} + +/* 내 비즈니스 카드 */ +.myinfo-business-card { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 12px; + padding: 1.5rem; +} + +.myinfo-business-empty-title { + font-size: 1.1rem; + font-weight: 600; + color: #FFFFFF; + margin-bottom: 0.5rem; +} + +.myinfo-business-empty-desc { + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.5); + margin-bottom: 1.25rem; +} + +.myinfo-business-input-row { + display: flex; + gap: 0.75rem; + align-items: center; +} + +.myinfo-business-input { + flex: 1; + padding: 0.75rem 1rem; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + color: #FFFFFF; + font-size: 0.9rem; +} + +.myinfo-business-input::placeholder { + color: rgba(255, 255, 255, 0.3); +} + +.myinfo-business-input:focus { + outline: none; + border-color: #a6ffea; +} + +.myinfo-business-submit { + padding: 0.75rem 1.25rem; + background: linear-gradient(135deg, #a6ffea 0%, #7ee8cf 100%); + border: none; + border-radius: 8px; + color: #002224; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + transition: all 0.2s ease; +} + +.myinfo-business-submit:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(166, 255, 234, 0.3); +} + +/* 소셜 채널 버튼들 (가로 배치) */ +.myinfo-social-buttons { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.myinfo-social-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1rem; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 8px; + color: #FFFFFF; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.myinfo-social-btn:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.25); +} + +.myinfo-social-btn.connected { + border-color: rgba(166, 255, 234, 0.4); + background: rgba(166, 255, 234, 0.08); +} + +.myinfo-social-btn:disabled, +.myinfo-social-btn.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.myinfo-social-btn-icon { + width: 20px; + height: 20px; + border-radius: 4px; + object-fit: cover; +} + +/* 연결된 계정 목록 */ +.myinfo-connected-accounts { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-top: 1.25rem; +} + +.myinfo-connected-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem; + background: rgba(166, 255, 234, 0.03); + border: 1px solid rgba(166, 255, 234, 0.15); + border-radius: 12px; +} + +.myinfo-connected-info { + display: flex; + align-items: center; + gap: 0.875rem; +} + +.myinfo-connected-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; +} + +.myinfo-connected-icon { + width: 40px; + height: 40px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.myinfo-connected-icon img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.myinfo-connected-text { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.myinfo-connected-channel { + font-size: 0.9rem; + font-weight: 600; + color: #FFFFFF; +} + +.myinfo-connected-platform { + font-size: 0.7rem; + color: rgba(255, 255, 255, 0.5); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.myinfo-connected-actions { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.myinfo-connected-badge { + display: flex; + align-items: center; + gap: 0.375rem; + font-size: 0.75rem; + font-weight: 500; + color: #a6ffea; +} + +.myinfo-connected-disconnect { + padding: 0.375rem 0.75rem; + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 6px; + color: rgba(255, 255, 255, 0.6); + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.myinfo-connected-disconnect:hover { + background: rgba(248, 113, 113, 0.1); + border-color: rgba(248, 113, 113, 0.3); + color: #f87171; +} + +.myinfo-loading { + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.5); + text-align: center; + padding: 0.5rem; + margin-top: 1rem; +} + +/* 모바일 반응형 */ +@media (max-width: 768px) { + .myinfo-page { + padding: 1.25rem; + } + + .myinfo-title { + font-size: 1.5rem; + } + + .myinfo-tabs { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + } + + .myinfo-tabs::-webkit-scrollbar { + display: none; + } + + .myinfo-tab { + white-space: nowrap; + padding: 0.625rem 0.75rem; + font-size: 0.8rem; + } + + .myinfo-business-input-row { + flex-direction: column; + } + + .myinfo-business-submit { + width: 100%; + } +} + +/* ===================================================== + Content Upload Button (ADO2 콘텐츠 페이지) + ===================================================== */ + +.content-upload-btn { + width: 34px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + background-color: #01393B; + border: none; + border-radius: 8px; + color: #6AB0B3; + cursor: pointer; + transition: background-color 0.2s; +} + +.content-upload-btn:hover:not(:disabled) { + background-color: #024648; +} + +.content-upload-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.content-upload-btn svg { + stroke: #6AB0B3; +} + +/* ===================================================== + Social Posting Modal + ===================================================== */ + +.social-posting-overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 1rem; +} + +.social-posting-modal { + width: 100%; + max-width: 900px; + max-height: 90vh; + background-color: #002224; + border-radius: 16px; + border: 1px solid rgba(166, 255, 234, 0.1); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.social-posting-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.25rem 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.social-posting-title { + font-size: 1.125rem; + font-weight: 600; + color: #FFFFFF; + margin: 0; +} + +.social-posting-close { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + color: rgba(255, 255, 255, 0.5); + cursor: pointer; + border-radius: 6px; + transition: all 0.2s; +} + +.social-posting-close:hover { + color: #FFFFFF; + background: rgba(255, 255, 255, 0.1); +} + +.social-posting-content { + display: flex; + flex: 1; + overflow: hidden; +} + +@media (max-width: 768px) { + .social-posting-content { + flex-direction: column; + } +} + +/* Video Preview Section */ +.social-posting-preview { + width: 45%; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; + background-color: #001a1c; + border-right: 1px solid rgba(255, 255, 255, 0.08); +} + +@media (max-width: 768px) { + .social-posting-preview { + width: 100%; + max-height: 40vh; + border-right: none; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + } +} + +.social-posting-video-container { + width: 100%; + max-width: 280px; + aspect-ratio: 9/16; + background-color: #000; + border-radius: 12px; + overflow: hidden; +} + +.social-posting-video { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* Form Section */ +.social-posting-form { + flex: 1; + padding: 1.5rem; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.social-posting-video-info { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.social-posting-label-badge { + padding: 0.375rem 0.75rem; + background: rgba(166, 130, 255, 0.15); + border: 1px solid rgba(166, 130, 255, 0.3); + border-radius: 6px; + font-size: 0.75rem; + font-weight: 500; + color: #a682ff; +} + +.social-posting-add-btn { + width: 26px; + height: 26px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px dashed rgba(255, 255, 255, 0.2); + border-radius: 6px; + color: rgba(255, 255, 255, 0.4); + font-size: 1rem; + cursor: pointer; +} + +.social-posting-video-meta { + padding: 0.75rem 1rem; + background: rgba(255, 255, 255, 0.03); + border-radius: 8px; +} + +.social-posting-video-title { + font-size: 0.875rem; + font-weight: 500; + color: #FFFFFF; + margin: 0 0 0.25rem 0; +} + +.social-posting-video-specs { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.5); + margin: 0; +} + +.social-posting-field { + display: flex; + flex-direction: column; + gap: 0.5rem; + position: relative; +} + +.social-posting-label { + font-size: 0.8rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.8); +} + +.social-posting-label .required { + color: #a6ffea; +} + +.social-posting-input, +.social-posting-select, +.social-posting-textarea { + width: 100%; + padding: 0.75rem 1rem; + background-color: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + color: #FFFFFF; + font-size: 0.875rem; + font-family: 'Pretendard', sans-serif; + transition: border-color 0.2s; +} + +.social-posting-input::placeholder, +.social-posting-textarea::placeholder { + color: rgba(255, 255, 255, 0.3); +} + +.social-posting-input:focus, +.social-posting-select:focus, +.social-posting-textarea:focus { + outline: none; + border-color: rgba(166, 255, 234, 0.4); +} + +.social-posting-select { + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='rgba(255,255,255,0.5)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 1rem center; + padding-right: 2.5rem; +} + +.social-posting-select option { + background-color: #002224; + color: #FFFFFF; +} + +.social-posting-textarea { + resize: vertical; + min-height: 80px; +} + +.social-posting-char-count { + position: absolute; + right: 0; + top: 0; + font-size: 0.7rem; + color: rgba(255, 255, 255, 0.4); +} + +.social-posting-loading, +.social-posting-no-accounts { + padding: 1rem; + text-align: center; + color: rgba(255, 255, 255, 0.5); + font-size: 0.85rem; +} + +.social-posting-no-accounts-hint { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.4); + margin-top: 0.25rem; +} + +/* Radio Group */ +.social-posting-radio-group { + display: flex; + gap: 1.25rem; +} + +.social-posting-radio { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; +} + +.social-posting-radio input[type="radio"] { + width: 16px; + height: 16px; + accent-color: #a6ffea; + cursor: pointer; +} + +.social-posting-radio .radio-label { + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.8); +} + +.social-posting-radio .radio-label.disabled { + color: rgba(255, 255, 255, 0.4); +} + +/* Footer */ +.social-posting-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.5rem; + border-top: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(0, 0, 0, 0.2); +} + +.social-posting-footer-note { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.5); + margin: 0; +} + +.social-posting-link { + color: #a6ffea; + text-decoration: underline; +} + +.social-posting-actions { + display: flex; + gap: 0.75rem; +} + +.social-posting-btn { + padding: 0.625rem 1.25rem; + border-radius: 8px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.social-posting-btn.cancel { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.15); + color: rgba(255, 255, 255, 0.7); +} + +.social-posting-btn.cancel:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.25); +} + +.social-posting-btn.submit { + background: linear-gradient(135deg, #a682ff 0%, #8b5cf6 100%); + border: none; + color: #FFFFFF; +} + +.social-posting-btn.submit:hover:not(:disabled) { + background: linear-gradient(135deg, #9570f0 0%, #7c4fe0 100%); +} + +.social-posting-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Mobile Adjustments */ +@media (max-width: 768px) { + .social-posting-modal { + max-height: 95vh; + } + + .social-posting-header { + padding: 1rem; + } + + .social-posting-form { + padding: 1rem; + } + + .social-posting-footer { + flex-direction: column; + gap: 1rem; + padding: 1rem; + } + + .social-posting-footer-note { + text-align: center; + } + + .social-posting-actions { + width: 100%; + } + + .social-posting-btn { + flex: 1; + } + + .social-posting-radio-group { + flex-direction: column; + gap: 0.75rem; + } +} + +/* Custom Scrollbar for Social Posting Modal */ +.social-posting-modal::-webkit-scrollbar, +.social-posting-content::-webkit-scrollbar, +.social-posting-form::-webkit-scrollbar { + width: 6px; +} + +.social-posting-modal::-webkit-scrollbar-track, +.social-posting-content::-webkit-scrollbar-track, +.social-posting-form::-webkit-scrollbar-track { + background: #001a1c; +} + +.social-posting-modal::-webkit-scrollbar-thumb, +.social-posting-content::-webkit-scrollbar-thumb, +.social-posting-form::-webkit-scrollbar-thumb { + background: #003d40; + border-radius: 3px; +} + +.social-posting-modal::-webkit-scrollbar-thumb:hover, +.social-posting-content::-webkit-scrollbar-thumb:hover, +.social-posting-form::-webkit-scrollbar-thumb:hover { + background: #004d50; +} + +/* Firefox scrollbar */ +.social-posting-modal, +.social-posting-content, +.social-posting-form { + scrollbar-width: thin; + scrollbar-color: #003d40 #001a1c; +} + +/* Custom Channel Dropdown */ +.social-posting-channel-dropdown { + position: relative; +} + +.social-posting-channel-trigger { + width: 100%; + padding: 0.75rem 1rem; + background-color: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + color: #FFFFFF; + font-size: 0.875rem; + font-family: 'Pretendard', sans-serif; + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + transition: border-color 0.2s; +} + +.social-posting-channel-trigger:hover { + border-color: rgba(255, 255, 255, 0.2); +} + +.social-posting-channel-trigger.open { + border-color: rgba(166, 255, 234, 0.4); +} + +.social-posting-channel-selected { + display: flex; + align-items: center; + gap: 0.625rem; +} + +.social-posting-channel-icon { + width: 24px; + height: 24px; + border-radius: 4px; + object-fit: cover; +} + +.social-posting-channel-arrow { + width: 12px; + height: 12px; + transition: transform 0.2s; +} + +.social-posting-channel-trigger.open .social-posting-channel-arrow { + transform: rotate(180deg); +} + +.social-posting-channel-menu { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: 4px; + background-color: #003538; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + overflow: hidden; + z-index: 10; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.social-posting-channel-option { + width: 100%; + padding: 0.75rem 1rem; + background: transparent; + border: none; + color: #FFFFFF; + font-size: 0.875rem; + font-family: 'Pretendard', sans-serif; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.625rem; + transition: background-color 0.15s; +} + +.social-posting-channel-option:hover { + background-color: rgba(166, 255, 234, 0.1); +} + +.social-posting-channel-option.selected { + background-color: rgba(166, 255, 234, 0.15); +} + +.social-posting-channel-option-icon { + width: 24px; + height: 24px; + border-radius: 4px; + object-fit: cover; +} + + +/* ============================================ + Upload Progress Modal Styles + ============================================ */ + +.upload-progress-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1100; + padding: 1rem; +} + +.upload-progress-modal { + width: 100%; + max-width: 420px; + background: linear-gradient(180deg, #003538 0%, #002224 100%); + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.1); + overflow: hidden; +} + +.upload-progress-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.25rem 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.upload-progress-title { + font-size: 1.125rem; + font-weight: 600; + color: #FFFFFF; + margin: 0; +} + +.upload-progress-close { + background: transparent; + border: none; + color: rgba(255, 255, 255, 0.5); + cursor: pointer; + padding: 0.25rem; + display: flex; + transition: color 0.2s; +} + +.upload-progress-close:hover { + color: #FFFFFF; +} + +.upload-progress-content { + padding: 2rem 1.5rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 1.25rem; +} + +/* Spinner */ +.upload-progress-spinner { + width: 64px; + height: 64px; +} + +.upload-spinner-svg { + width: 100%; + height: 100%; + animation: upload-spinner-rotate 1.5s linear infinite; +} + +.upload-spinner-svg circle { + stroke: #a6ffea; + stroke-dasharray: 90, 150; + stroke-dashoffset: 0; + stroke-linecap: round; + animation: upload-spinner-dash 1.5s ease-in-out infinite; +} + +@keyframes upload-spinner-rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes upload-spinner-dash { + 0% { + stroke-dasharray: 1, 150; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -35; + } + 100% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -124; + } +} + +/* Status Icons */ +.upload-progress-icon { + width: 64px; + height: 64px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.upload-progress-icon.success { + background: rgba(166, 255, 234, 0.15); + color: #a6ffea; +} + +.upload-progress-icon.error { + background: rgba(248, 113, 113, 0.15); + color: #f87171; +} + +/* Status Text */ +.upload-progress-status { + font-size: 1.25rem; + font-weight: 600; + color: #FFFFFF; + margin: 0; +} + +.upload-progress-status.completed { + color: #a6ffea; +} + +.upload-progress-status.failed { + color: #f87171; +} + +/* Progress Bar */ +.upload-progress-bar-container { + width: 100%; + max-width: 280px; + position: relative; +} + +.upload-progress-bar-container::before { + content: ''; + display: block; + width: 100%; + height: 8px; + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; +} + +.upload-progress-bar-fill { + position: absolute; + top: 0; + left: 0; + height: 8px; + background: linear-gradient(90deg, #a6ffea 0%, #4fd1c5 100%); + border-radius: 4px; + transition: width 0.3s ease; +} + +.upload-progress-percent { + display: block; + text-align: center; + margin-top: 0.5rem; + font-size: 0.875rem; + color: rgba(255, 255, 255, 0.7); +} + +/* Info Section */ +.upload-progress-info { + width: 100%; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + padding: 1rem; +} + +.upload-progress-info-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; +} + +.upload-progress-info-row + .upload-progress-info-row { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.upload-progress-label { + font-size: 0.8125rem; + color: rgba(255, 255, 255, 0.5); + flex-shrink: 0; +} + +.upload-progress-value { + font-size: 0.875rem; + color: #FFFFFF; + text-align: right; + word-break: break-word; +} + +/* Error Message */ +.upload-progress-error { + width: 100%; + background: rgba(248, 113, 113, 0.1); + border: 1px solid rgba(248, 113, 113, 0.3); + border-radius: 8px; + padding: 0.875rem 1rem; +} + +.upload-progress-error p { + margin: 0; + font-size: 0.875rem; + color: #f87171; + text-align: center; +} + +/* YouTube Link */ +.upload-progress-youtube-link { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + background: #FF0000; + color: #FFFFFF; + font-size: 0.875rem; + font-weight: 500; + text-decoration: none; + border-radius: 8px; + transition: background-color 0.2s; +} + +.upload-progress-youtube-link:hover { + background: #CC0000; +} + +.upload-youtube-icon { + width: 20px; + height: 20px; +} + +/* Footer */ +.upload-progress-footer { + padding: 1.25rem 1.5rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + justify-content: center; +} + +.upload-progress-btn { + padding: 0.75rem 2rem; + border-radius: 8px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.upload-progress-btn.primary { + background: linear-gradient(135deg, #a6ffea 0%, #4fd1c5 100%); + border: none; + color: #002224; +} + +.upload-progress-btn.primary:hover { + opacity: 0.9; + transform: translateY(-1px); +} + +.upload-progress-note { + margin: 0; + font-size: 0.8125rem; + color: rgba(255, 255, 255, 0.5); + text-align: center; +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index b1cedef..b137257 100755 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -58,10 +58,8 @@ const Sidebar: React.FC = ({ activeItem, onNavigate, onHome }) => { id: '대시보드', label: '대시보드', disabled: false, icon: }, { id: '새 프로젝트 만들기', label: '새 프로젝트 만들기', disabled: false, icon: }, { id: 'ADO2 콘텐츠', label: 'ADO2 콘텐츠', disabled: false, icon: }, - { id: '에셋 관리', label: '에셋 관리', disabled: true, icon: }, - { id: '내 펜션', label: '내 펜션', disabled: true, icon: }, - { id: '계정 설정', label: '계정 설정', disabled: true, icon: }, - { id: '비즈니스 설정', label: '비즈니스 설정', disabled: true, icon: }, + { id: '내 콘텐츠', label: '내 콘텐츠', disabled: true, icon: }, + { id: '내 정보', label: '내 정보', disabled: false, icon: }, ]; return ( diff --git a/src/components/SocialPostingModal.tsx b/src/components/SocialPostingModal.tsx new file mode 100644 index 0000000..2ff87c2 --- /dev/null +++ b/src/components/SocialPostingModal.tsx @@ -0,0 +1,487 @@ + +import React, { useState, useEffect, useRef } from 'react'; +import { getSocialAccounts, uploadToSocial, waitForUploadComplete } from '../utils/api'; +import { SocialAccount, VideoListItem, SocialUploadStatusResponse } from '../types/api'; +import UploadProgressModal, { UploadStatus } from './UploadProgressModal'; + +interface SocialPostingModalProps { + isOpen: boolean; + onClose: () => void; + video: VideoListItem | null; +} + +type PrivacyType = 'public' | 'unlisted' | 'private'; +type PublishTimeType = 'now' | 'schedule'; + +// 플랫폼별 아이콘 경로 +const getPlatformIcon = (platform: string) => { + switch (platform) { + case 'youtube': + return '/assets/images/social-youtube.png'; + case 'instagram': + return '/assets/images/social-instagram.png'; + default: + return '/assets/images/social-youtube.png'; + } +}; + +const SocialPostingModal: React.FC = ({ + isOpen, + onClose, + video +}) => { + const [socialAccounts, setSocialAccounts] = useState([]); + const [selectedChannel, setSelectedChannel] = useState(''); + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [tags, setTags] = useState(''); + const [privacy, setPrivacy] = useState('public'); + const [publishTime, setPublishTime] = useState('now'); + const [isLoadingAccounts, setIsLoadingAccounts] = useState(false); + const [isPosting, setIsPosting] = useState(false); + const [isChannelDropdownOpen, setIsChannelDropdownOpen] = useState(false); + const [isPrivacyDropdownOpen, setIsPrivacyDropdownOpen] = useState(false); + const channelDropdownRef = useRef(null); + const privacyDropdownRef = useRef(null); + + // Upload progress modal state + const [showUploadProgress, setShowUploadProgress] = useState(false); + const [uploadStatus, setUploadStatus] = useState('pending'); + const [uploadProgress, setUploadProgress] = useState(0); + const [uploadYoutubeUrl, setUploadYoutubeUrl] = useState(); + const [uploadErrorMessage, setUploadErrorMessage] = useState(); + // 업로드 정보 (모달이 닫힌 후에도 유지) + const [uploadVideoTitle, setUploadVideoTitle] = useState(''); + const [uploadChannelName, setUploadChannelName] = useState(''); + + // 드롭다운 외부 클릭 시 닫기 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (channelDropdownRef.current && !channelDropdownRef.current.contains(event.target as Node)) { + setIsChannelDropdownOpen(false); + } + if (privacyDropdownRef.current && !privacyDropdownRef.current.contains(event.target as Node)) { + setIsPrivacyDropdownOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + // 소셜 계정 로드 + useEffect(() => { + if (isOpen) { + loadSocialAccounts(); + // 비디오 정보로 기본 제목 설정 + if (video) { + const date = new Date(video.created_at); + const formattedDate = `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`; + setTitle(`${video.store_name} ${formattedDate}`); + } + } + }, [isOpen, video]); + + const loadSocialAccounts = async () => { + setIsLoadingAccounts(true); + try { + const response = await getSocialAccounts(); + const activeAccounts = response.accounts?.filter(acc => acc.is_active) || []; + setSocialAccounts(activeAccounts); + if (activeAccounts.length > 0) { + setSelectedChannel(activeAccounts[0].platform_user_id); + } + } catch (error) { + console.error('Failed to load social accounts:', error); + } finally { + setIsLoadingAccounts(false); + } + }; + + const handlePost = async () => { + if (!selectedChannel || !title.trim() || !video) { + alert('채널과 제목을 입력해주세요.'); + return; + } + + const selectedAcc = socialAccounts.find(acc => acc.platform_user_id === selectedChannel); + if (!selectedAcc) { + alert('채널을 선택해주세요.'); + return; + } + + // video.video_id 검증 - 반드시 존재해야 함 + if (!video.video_id) { + console.error('Video object missing video_id:', video); + alert('영상 정보가 올바르지 않습니다. (video_id 누락)'); + return; + } + + setIsPosting(true); + + // Reset upload progress state + setUploadStatus('pending'); + setUploadProgress(0); + setUploadYoutubeUrl(undefined); + setUploadErrorMessage(undefined); + // 업로드 정보 저장 (모달이 닫힌 후에도 유지) + setUploadVideoTitle(title.trim()); + setUploadChannelName(selectedAcc.display_name); + setShowUploadProgress(true); + + try { + // Parse tags from comma-separated string + const tagsArray = tags + .split(',') + .map(tag => tag.trim()) + .filter(tag => tag.length > 0); + + // Request payload 로그 + const requestPayload = { + video_id: video.video_id, + social_account_id: selectedAcc.id, + title: title.trim(), + description: description.trim(), + tags: tagsArray, + privacy_status: privacy, + scheduled_at: publishTime === 'now' ? null : null, + }; + console.log('[Upload] Request payload:', requestPayload); + + // Call upload API + const uploadResponse = await uploadToSocial(requestPayload); + + if (!uploadResponse.success) { + throw new Error(uploadResponse.message || '업로드 시작에 실패했습니다.'); + } + + // Poll for upload completion + const result = await waitForUploadComplete( + uploadResponse.upload_id, + (status, progress) => { + setUploadStatus(status as UploadStatus); + setUploadProgress(progress || 0); + } + ); + + // Upload completed successfully + setUploadStatus('completed'); + setUploadProgress(100); + setUploadYoutubeUrl(result.platform_url); + + // Close the posting modal (keep progress modal open) + onClose(); + resetForm(); + } catch (error) { + console.error('Upload failed:', error); + setUploadStatus('failed'); + setUploadErrorMessage(error instanceof Error ? error.message : '업로드에 실패했습니다.'); + } finally { + setIsPosting(false); + } + }; + + const resetForm = () => { + setTitle(''); + setDescription(''); + setTags(''); + setPrivacy('public'); + setPublishTime('now'); + setSelectedChannel(''); + setIsChannelDropdownOpen(false); + setIsPrivacyDropdownOpen(false); + }; + + const handleUploadProgressClose = () => { + setShowUploadProgress(false); + setUploadStatus('pending'); + setUploadProgress(0); + setUploadYoutubeUrl(undefined); + setUploadErrorMessage(undefined); + setUploadVideoTitle(''); + setUploadChannelName(''); + }; + + const handleClose = () => { + onClose(); + resetForm(); + }; + + const selectedAccount = socialAccounts.find(acc => acc.platform_user_id === selectedChannel); + + const privacyOptions = [ + { value: 'public', label: '공개' }, + { value: 'unlisted', label: '미등록 (링크로만 접근)' }, + { value: 'private', label: '비공개' } + ]; + + const selectedPrivacyOption = privacyOptions.find(opt => opt.value === privacy); + + // Always render UploadProgressModal, even when main modal is closed + const uploadProgressModalElement = ( + + ); + + if (!isOpen || !video) { + // Still render upload progress modal even when main modal is closed + return showUploadProgress ? uploadProgressModalElement : null; + } + + return ( + <> +
+
e.stopPropagation()}> + {/* Header */} +
+

소셜 미디어 포스팅

+ +
+ + {/* Content */} +
+ {/* Left: Video Preview */} +
+
+
+
+ + {/* Right: Form */} +
+ {/* Video Info */} +
+ 게시물 1 + + +
+ +
+

+ {video.store_name} {new Date(video.created_at).toLocaleString('ko-KR')} +

+

1080x1920 · 10초

+
+ + {/* Channel Selector - Custom Dropdown */} +
+ + {isLoadingAccounts ? ( +
계정 로딩 중...
+ ) : socialAccounts.length === 0 ? ( +
+

연결된 소셜 계정이 없습니다.

+

내 정보에서 계정을 연결해주세요.

+
+ ) : ( +
+ + {isChannelDropdownOpen && ( +
+ {socialAccounts.map(account => ( + + ))} +
+ )} +
+ )} +
+ + {/* Title */} +
+ + setTitle(e.target.value)} + placeholder="게시물 제목을 입력하세요." + className="social-posting-input" + maxLength={100} + /> + {title.length}/100 +
+ + {/* Description */} +
+ +