hbyang 2026-03-09 15:45:27 +09:00
commit fa4864e9ef
6 changed files with 329 additions and 390 deletions

160
index.css
View File

@ -2049,19 +2049,23 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
padding: 8px 20px 8px 8px; background-color: #462E64;
background: #462E64; color: #CFABFB;
font-size: 0.875rem;
font-weight: 600;
padding: 0 1.25rem 0 0.5rem;
height: 36px;
border: 1px solid #694596; border: 1px solid #694596;
border-radius: 999px; border-radius: 999px;
color: #CFABFB;
font-size: 14px;
font-weight: 600;
cursor: pointer; cursor: pointer;
transition: opacity 0.2s; white-space: nowrap;
transition: background-color 0.2s;
line-height: 1.19;
letter-spacing: -0.006em;
} }
.comp2-back-btn:hover { .comp2-back-btn:hover {
opacity: 0.85; background-color: #5a3a80;
} }
.comp2-container { .comp2-container {
@ -3270,18 +3274,40 @@
.bi2-page { .bi2-page {
min-height: 100vh; min-height: 100vh;
background-color: #002224; background: linear-gradient(to bottom, #002224, #01191a);
color: #E5F1F2; color: #E5F1F2;
padding-top: 64px;
padding-bottom: 160px; padding-bottom: 160px;
font-family: 'Pretendard', sans-serif; font-family: 'Pretendard', sans-serif;
overflow-x: auto;
} }
/* 헤더 */ /* 헤더 */
.bi2-header { .bi2-header {
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 8px 32px; justify-content: flex-start;
padding: 8px 1rem;
height: 64px; height: 64px;
z-index: 30;
}
@media (min-width: 768px) {
.bi2-header {
padding: 8px 2rem;
}
body:has(.sidebar.expanded) .bi2-header {
left: 15rem;
}
body:has(.sidebar.collapsed) .bi2-header {
left: 5rem;
}
} }
.bi2-back-btn { .bi2-back-btn {
@ -3316,13 +3342,14 @@
} }
.bi2-title-icon { .bi2-title-icon {
width: 40px; width: 80px;
height: 40px; height: 80px;
} }
.bi2-star-icon { .bi2-star-icon {
width: 40px; width: 80px;
height: 40px; height: 80px;
aspect-ratio: 1/1;
animation: twinkle 2.5s ease-in-out infinite; animation: twinkle 2.5s ease-in-out infinite;
} }
@ -3334,7 +3361,7 @@
} }
.bi2-main-title { .bi2-main-title {
font-size: 40px; font-size: 48px;
font-weight: 600; font-weight: 600;
color: #FFFFFF; color: #FFFFFF;
letter-spacing: -0.006em; letter-spacing: -0.006em;
@ -3354,6 +3381,7 @@
/* 메인 컨테이너 */ /* 메인 컨테이너 */
.bi2-main-container { .bi2-main-container {
max-width: 1440px; max-width: 1440px;
min-width: 1000px;
margin: 0 auto; margin: 0 auto;
background: #013032; background: #013032;
border: 1px solid #034A4D; border: 1px solid #034A4D;
@ -3404,11 +3432,13 @@
/* 브랜드 정체성 카드 */ /* 브랜드 정체성 카드 */
.bi2-identity-card { .bi2-identity-card {
background: #034245; background: #034245;
border: 1px solid #94FBE0;
border-radius: 20px; border-radius: 20px;
padding: 22px 24px; padding: 22px 24px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 22px; gap: 22px;
box-shadow: 0 0 40px rgba(148, 251, 224, 0.1);
} }
.bi2-identity-top { .bi2-identity-top {
@ -3433,7 +3463,7 @@
.bi2-core-value { .bi2-core-value {
font-size: 24px; font-size: 24px;
font-weight: 700; font-weight: 700;
color: #FFFFFF; color: #F4FFFC;
letter-spacing: -0.006em; letter-spacing: -0.006em;
line-height: 1.3; line-height: 1.3;
} }
@ -3464,10 +3494,10 @@
} }
.bi2-body-text { .bi2-body-text {
font-size: 16px; font-size: 18px;
font-weight: 400; font-weight: 400;
color: #CEE5E6; color: #CEE5E6;
line-height: 1.625; line-height: 1.6;
letter-spacing: -0.006em; letter-spacing: -0.006em;
} }
@ -3476,7 +3506,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 160px; gap: clamp(20px, 4vw, 80px);
background: #034245; background: #034245;
border: 1px solid #034A4D; border: 1px solid #034A4D;
border-radius: 20px; border-radius: 20px;
@ -3493,6 +3523,17 @@
align-items: center; align-items: center;
} }
@media (max-width: 768px) {
.bi2-radar-container svg {
width: min(95vw, 420px) !important;
height: min(95vw, 420px) !important;
}
.bi2-radar-label-text {
display: none;
}
}
.bi2-selling-list { .bi2-selling-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -3532,19 +3573,19 @@
} }
.bi2-selling-name { .bi2-selling-name {
font-size: 16px; font-size: 20px;
font-weight: 400; font-weight: 600;
color: #E5F1F2; color: #E5F1F2;
letter-spacing: -0.006em; letter-spacing: -0.006em;
line-height: 1.375; line-height: 1.375;
} }
.bi2-selling-desc { .bi2-selling-desc {
font-size: 12px; font-size: 14px;
font-weight: 500; font-weight: 600;
color: #9BCACC; color: #9BCACC;
letter-spacing: -0.006em; letter-spacing: -0.006em;
line-height: 1; line-height: 1.29;
} }
/* 고객 유형 카드 */ /* 고객 유형 카드 */
@ -3629,9 +3670,9 @@
.bi2-persona-detail-text { .bi2-persona-detail-text {
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 600;
color: #E5F1F2; color: #E5F1F2;
line-height: 1.625; line-height: 26px;
letter-spacing: -0.006em; letter-spacing: -0.006em;
white-space: pre-line; white-space: pre-line;
text-align: right; text-align: right;
@ -3665,18 +3706,67 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 8px 16px; padding: 8px 16px;
background: #034245; background: #01393B;
border-radius: 999px; border-radius: 999px;
font-size: 16px; font-size: 20px;
font-weight: 400; font-weight: 600;
color: #E5F1F2; color: #E5F1F2;
letter-spacing: -0.006em; letter-spacing: -0.006em;
line-height: 1.375; line-height: 1.375;
} }
/* 반응형 */ /* 하단 고정 버튼 */
@media (max-width: 1024px) { .bi2-bottom-button-container {
position: fixed;
bottom: 32px;
left: 0;
right: 0;
display: flex;
justify-content: center;
z-index: 50;
pointer-events: none;
}
@media (min-width: 768px) {
body:has(.sidebar.expanded) .bi2-bottom-button-container {
left: 15rem;
}
body:has(.sidebar.collapsed) .bi2-bottom-button-container {
left: 5rem;
}
}
.bi2-generate-btn {
pointer-events: auto;
display: flex;
align-items: center;
gap: 8px;
padding: 12px 48px;
background: #A65EFF;
color: #FFFFFF;
font-size: 16px;
font-weight: 700;
border: none;
border-radius: 999px;
cursor: pointer;
box-shadow: 0 4px 24px rgba(174, 114, 249, 0.4);
transition: background-color 0.2s, transform 0.2s;
}
.bi2-generate-btn:hover {
background: #8B3FE8;
transform: scale(1.05);
}
/* 반응형 태블릿/모바일 (≤768px) */
@media (max-width: 768px) {
.bi2-page {
padding-top: 64px;
}
.bi2-main-container { .bi2-main-container {
min-width: unset;
margin: 0 16px; margin: 0 16px;
padding: 24px 20px 80px; padding: 24px 20px 80px;
gap: 48px; gap: 48px;
@ -3704,6 +3794,14 @@
.bi2-store-name { .bi2-store-name {
font-size: 24px; font-size: 24px;
} }
.bi2-selling-name {
font-size: 16px;
}
.bi2-keyword-pill {
font-size: 16px;
}
} }
/* ===================================================== /* =====================================================
@ -7857,7 +7955,7 @@
.lyrics-textarea { .lyrics-textarea {
width: 100%; width: 100%;
height: 200px; min-height: 200px;
background: transparent; background: transparent;
border: none; border: none;
color: var(--color-text-white); color: var(--color-text-white);

View File

@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.0001 7.5C15.2717 7.50006 15.5359 7.58858 15.7527 7.75216C15.9695 7.91575 16.1271 8.14551 16.2017 8.40667L17.5567 13.15C17.8485 14.1715 18.3958 15.1018 19.147 15.853C19.8982 16.6043 20.8285 17.1516 21.8501 17.4433L26.5934 18.7983C26.8544 18.8731 27.084 19.0308 27.2474 19.2476C27.4108 19.4644 27.4992 19.7285 27.4992 20C27.4992 20.2715 27.4108 20.5356 27.2474 20.7524C27.084 20.9692 26.8544 21.1269 26.5934 21.2017L21.8501 22.5567C20.8285 22.8484 19.8982 23.3957 19.147 24.147C18.3958 24.8982 17.8485 25.8285 17.5567 26.85L16.2017 31.5933C16.127 31.8543 15.9693 32.0839 15.7525 32.2473C15.5357 32.4107 15.2716 32.4991 15.0001 32.4991C14.7286 32.4991 14.4645 32.4107 14.2477 32.2473C14.0309 32.0839 13.8732 31.8543 13.7984 31.5933L12.4434 26.85C12.1517 25.8285 11.6043 24.8982 10.8531 24.147C10.1019 23.3957 9.17161 22.8484 8.15008 22.5567L3.40674 21.2017C3.14575 21.1269 2.91619 20.9692 2.75278 20.7524C2.58937 20.5356 2.50098 20.2715 2.50098 20C2.50098 19.7285 2.58937 19.4644 2.75278 19.2476C2.91619 19.0308 3.14575 18.8731 3.40674 18.7983L8.15008 17.4433C9.17161 17.1516 10.1019 16.6043 10.8531 15.853C11.6043 15.1018 12.1517 14.1715 12.4434 13.15L13.7984 8.40667C13.873 8.14551 14.0306 7.91575 14.2475 7.75216C14.4643 7.58858 14.7285 7.50006 15.0001 7.5ZM30.0001 2.5C30.2789 2.49984 30.5498 2.59294 30.7697 2.76448C30.9896 2.93601 31.1457 3.17614 31.2134 3.44667L31.6434 5.17333C32.0367 6.74 33.2601 7.96333 34.8267 8.35667L36.5534 8.78667C36.8245 8.8538 37.0652 9.00976 37.2373 9.22967C37.4094 9.44957 37.5029 9.72077 37.5029 10C37.5029 10.2792 37.4094 10.5504 37.2373 10.7703C37.0652 10.9902 36.8245 11.1462 36.5534 11.2133L34.8267 11.6433C33.2601 12.0367 32.0367 13.26 31.6434 14.8267L31.2134 16.5533C31.1463 16.8244 30.9903 17.0651 30.7704 17.2372C30.5505 17.4093 30.2793 17.5028 30.0001 17.5028C29.7208 17.5028 29.4496 17.4093 29.2297 17.2372C29.0098 17.0651 28.8539 16.8244 28.7867 16.5533L28.3567 14.8267C28.1644 14.0574 27.7667 13.3548 27.2059 12.7941C26.6452 12.2334 25.9427 11.8356 25.1734 11.6433L23.4467 11.2133C23.1757 11.1462 22.9349 10.9902 22.7629 10.7703C22.5908 10.5504 22.4973 10.2792 22.4973 10C22.4973 9.72077 22.5908 9.44957 22.7629 9.22967C22.9349 9.00976 23.1757 8.8538 23.4467 8.78667L25.1734 8.35667C25.9427 8.16435 26.6452 7.76658 27.2059 7.20587C27.7667 6.64517 28.1644 5.94262 28.3567 5.17333L28.7867 3.44667C28.8544 3.17614 29.0106 2.93601 29.2305 2.76448C29.4503 2.59294 29.7212 2.49984 30.0001 2.5ZM27.5001 25C27.7626 24.9999 28.0185 25.0824 28.2315 25.2358C28.4445 25.3893 28.6038 25.6059 28.6867 25.855L29.3434 27.8267C29.5934 28.5717 30.1767 29.1583 30.9234 29.4067L32.8951 30.065C33.1434 30.1484 33.3592 30.3076 33.5121 30.5202C33.665 30.7328 33.7472 30.9881 33.7472 31.25C33.7472 31.5119 33.665 31.7672 33.5121 31.9798C33.3592 32.1924 33.1434 32.3517 32.8951 32.435L30.9234 33.0933C30.1784 33.3433 29.5917 33.9267 29.3434 34.6733L28.6851 36.645C28.6017 36.8933 28.4425 37.1091 28.2299 37.262C28.0172 37.4149 27.762 37.4972 27.5001 37.4972C27.2382 37.4972 26.9829 37.4149 26.7703 37.262C26.5577 37.1091 26.3984 36.8933 26.3151 36.645L25.6567 34.6733C25.534 34.3055 25.3273 33.9712 25.0531 33.697C24.7789 33.4228 24.4446 33.2161 24.0767 33.0933L22.1051 32.435C21.8568 32.3517 21.641 32.1924 21.4881 31.9798C21.3352 31.7672 21.2529 31.5119 21.2529 31.25C21.2529 30.9881 21.3352 30.7328 21.4881 30.5202C21.641 30.3076 21.8568 30.1484 22.1051 30.065L24.0767 29.4067C24.8217 29.1567 25.4084 28.5733 25.6567 27.8267L26.3151 25.855C26.3979 25.6062 26.557 25.3898 26.7696 25.2363C26.9823 25.0829 27.2378 25.0002 27.5001 25Z" fill="#DFC7FD"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,3 +1,21 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="118" height="118" viewBox="0 0 118 118" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.0001 7.5C15.2717 7.50006 15.5359 7.58858 15.7527 7.75216C15.9695 7.91575 16.1271 8.14551 16.2017 8.40667L17.5567 13.15C17.8485 14.1715 18.3958 15.1018 19.147 15.853C19.8982 16.6043 20.8285 17.1516 21.8501 17.4433L26.5934 18.7983C26.8544 18.8731 27.084 19.0308 27.2474 19.2476C27.4108 19.4644 27.4992 19.7285 27.4992 20C27.4992 20.2715 27.4108 20.5356 27.2474 20.7524C27.084 20.9692 26.8544 21.1269 26.5934 21.2017L21.8501 22.5567C20.8285 22.8484 19.8982 23.3957 19.147 24.147C18.3958 24.8982 17.8485 25.8285 17.5567 26.85L16.2017 31.5933C16.127 31.8543 15.9693 32.0839 15.7525 32.2473C15.5357 32.4107 15.2716 32.4991 15.0001 32.4991C14.7286 32.4991 14.4645 32.4107 14.2477 32.2473C14.0309 32.0839 13.8732 31.8543 13.7984 31.5933L12.4434 26.85C12.1517 25.8285 11.6043 24.8982 10.8531 24.147C10.1019 23.3957 9.17161 22.8484 8.15008 22.5567L3.40674 21.2017C3.14575 21.1269 2.91619 20.9692 2.75278 20.7524C2.58937 20.5356 2.50098 20.2715 2.50098 20C2.50098 19.7285 2.58937 19.4644 2.75278 19.2476C2.91619 19.0308 3.14575 18.8731 3.40674 18.7983L8.15008 17.4433C9.17161 17.1516 10.1019 16.6043 10.8531 15.853C11.6043 15.1018 12.1517 14.1715 12.4434 13.15L13.7984 8.40667C13.873 8.14551 14.0306 7.91575 14.2475 7.75216C14.4643 7.58858 14.7285 7.50006 15.0001 7.5ZM30.0001 2.5C30.2789 2.49984 30.5498 2.59294 30.7697 2.76448C30.9896 2.93601 31.1457 3.17614 31.2134 3.44667L31.6434 5.17333C32.0367 6.74 33.2601 7.96333 34.8267 8.35667L36.5534 8.78667C36.8245 8.8538 37.0652 9.00976 37.2373 9.22967C37.4094 9.44957 37.5029 9.72077 37.5029 10C37.5029 10.2792 37.4094 10.5504 37.2373 10.7703C37.0652 10.9902 36.8245 11.1462 36.5534 11.2133L34.8267 11.6433C33.2601 12.0367 32.0367 13.26 31.6434 14.8267L31.2134 16.5533C31.1463 16.8244 30.9903 17.0651 30.7704 17.2372C30.5505 17.4093 30.2793 17.5028 30.0001 17.5028C29.7208 17.5028 29.4496 17.4093 29.2297 17.2372C29.0098 17.0651 28.8539 16.8244 28.7867 16.5533L28.3567 14.8267C28.1644 14.0574 27.7667 13.3548 27.2059 12.7941C26.6452 12.2334 25.9427 11.8356 25.1734 11.6433L23.4467 11.2133C23.1757 11.1462 22.9349 10.9902 22.7629 10.7703C22.5908 10.5504 22.4973 10.2792 22.4973 10C22.4973 9.72077 22.5908 9.44957 22.7629 9.22967C22.9349 9.00976 23.1757 8.8538 23.4467 8.78667L25.1734 8.35667C25.9427 8.16435 26.6452 7.76658 27.2059 7.20587C27.7667 6.64517 28.1644 5.94262 28.3567 5.17333L28.7867 3.44667C28.8544 3.17614 29.0106 2.93601 29.2305 2.76448C29.4503 2.59294 29.7212 2.49984 30.0001 2.5ZM27.5001 25C27.7626 24.9999 28.0185 25.0824 28.2315 25.2358C28.4445 25.3893 28.6038 25.6059 28.6867 25.855L29.3434 27.8267C29.5934 28.5717 30.1767 29.1583 30.9234 29.4067L32.8951 30.065C33.1434 30.1484 33.3592 30.3076 33.5121 30.5202C33.665 30.7328 33.7472 30.9881 33.7472 31.25C33.7472 31.5119 33.665 31.7672 33.5121 31.9798C33.3592 32.1924 33.1434 32.3517 32.8951 32.435L30.9234 33.0933C30.1784 33.3433 29.5917 33.9267 29.3434 34.6733L28.6851 36.645C28.6017 36.8933 28.4425 37.1091 28.2299 37.262C28.0172 37.4149 27.762 37.4972 27.5001 37.4972C27.2382 37.4972 26.9829 37.4149 26.7703 37.262C26.5577 37.1091 26.3984 36.8933 26.3151 36.645L25.6567 34.6733C25.534 34.3055 25.3273 33.9712 25.0531 33.697C24.7789 33.4228 24.4446 33.2161 24.0767 33.0933L22.1051 32.435C21.8568 32.3517 21.641 32.1924 21.4881 31.9798C21.3352 31.7672 21.2529 31.5119 21.2529 31.25C21.2529 30.9881 21.3352 30.7328 21.4881 30.5202C21.641 30.3076 21.8568 30.1484 22.1051 30.065L24.0767 29.4067C24.8217 29.1567 25.4084 28.5733 25.6567 27.8267L26.3151 25.855C26.3979 25.6062 26.557 25.3898 26.7696 25.2363C26.9823 25.0829 27.2378 25.0002 27.5001 25Z" fill="#DFC7FD"/> <g filter="url(#filter0_d_2381_2707)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.9982 30C49.5414 30.0001 50.0698 30.1771 50.5034 30.5043C50.9371 30.8315 51.2523 31.291 51.4015 31.8133L54.1115 41.3C54.695 43.3431 55.7897 45.2037 57.2921 46.7061C58.7945 48.2085 60.6551 49.3032 62.6982 49.8867L72.1849 52.5967C72.7068 52.7462 73.166 53.0616 73.4928 53.4952C73.8196 53.9288 73.9964 54.457 73.9964 55C73.9964 55.543 73.8196 56.0712 73.4928 56.5048C73.166 56.9384 72.7068 57.2538 72.1849 57.4033L62.6982 60.1133C60.6551 60.6968 58.7945 61.7915 57.2921 63.2939C55.7897 64.7963 54.695 66.6569 54.1115 68.7L51.4015 78.1867C51.252 78.7086 50.9366 79.1678 50.503 79.4946C50.0694 79.8214 49.5412 79.9982 48.9982 79.9982C48.4552 79.9982 47.927 79.8214 47.4934 79.4946C47.0598 79.1678 46.7444 78.7086 46.5949 78.1867L43.8849 68.7C43.3014 66.6569 42.2067 64.7963 40.7043 63.2939C39.2019 61.7915 37.3413 60.6968 35.2982 60.1133L25.8115 57.4033C25.2895 57.2538 24.8304 56.9384 24.5036 56.5048C24.1768 56.0712 24 55.543 24 55C24 54.457 24.1768 53.9288 24.5036 53.4952C24.8304 53.0616 25.2895 52.7462 25.8115 52.5967L35.2982 49.8867C37.3413 49.3032 39.2019 48.2085 40.7043 46.7061C42.2067 45.2037 43.3014 43.3431 43.8849 41.3L46.5949 31.8133C46.7441 31.291 47.0593 30.8315 47.493 30.5043C47.9266 30.1771 48.455 30.0001 48.9982 30ZM78.9982 20C79.5559 19.9997 80.0977 20.1859 80.5374 20.529C80.9771 20.872 81.2895 21.3523 81.4249 21.8933L82.2849 25.3467C83.0715 28.48 85.5182 30.9267 88.6515 31.7133L92.1049 32.5733C92.6469 32.7076 93.1285 33.0195 93.4726 33.4593C93.8168 33.8991 94.0038 34.4415 94.0038 35C94.0038 35.5585 93.8168 36.1009 93.4726 36.5407C93.1285 36.9805 92.6469 37.2924 92.1049 37.4267L88.6515 38.2867C85.5182 39.0733 83.0715 41.52 82.2849 44.6533L81.4249 48.1067C81.2906 48.6488 80.9787 49.1303 80.5389 49.4744C80.099 49.8186 79.5567 50.0056 78.9982 50.0056C78.4397 50.0056 77.8973 49.8186 77.4575 49.4744C77.0177 49.1303 76.7058 48.6488 76.5715 48.1067L75.7115 44.6533C75.3269 43.1148 74.5314 41.7097 73.4099 40.5882C72.2885 39.4668 70.8834 38.6713 69.3449 38.2867L65.8915 37.4267C65.3494 37.2924 64.8679 36.9805 64.5238 36.5407C64.1796 36.1009 63.9926 35.5585 63.9926 35C63.9926 34.4415 64.1796 33.8991 64.5238 33.4593C64.8679 33.0195 65.3494 32.7076 65.8915 32.5733L69.3449 31.7133C70.8834 31.3287 72.2885 30.5332 73.4099 29.4117C74.5314 28.2903 75.3269 26.8852 75.7115 25.3467L76.5715 21.8933C76.7069 21.3523 77.0192 20.872 77.459 20.529C77.8987 20.1859 78.4405 19.9997 78.9982 20ZM73.9982 65C74.5232 64.9997 75.0351 65.1647 75.4611 65.4716C75.8871 65.7786 76.2056 66.2118 76.3715 66.71L77.6849 70.6533C78.1849 72.1433 79.3515 73.3167 80.8449 73.8133L84.7882 75.13C85.2847 75.2967 85.7164 75.6152 86.0222 76.0404C86.328 76.4656 86.4925 76.9762 86.4925 77.5C86.4925 78.0238 86.328 78.5343 86.0222 78.9596C85.7164 79.3848 85.2847 79.7033 84.7882 79.87L80.8449 81.1867C79.3549 81.6867 78.1815 82.8533 77.6849 84.3467L76.3682 88.29C76.2015 88.7865 75.883 89.2182 75.4578 89.524C75.0325 89.8298 74.522 89.9943 73.9982 89.9943C73.4744 89.9943 72.9638 89.8298 72.5386 89.524C72.1133 89.2182 71.7949 88.7865 71.6282 88.29L70.3115 84.3467C70.066 83.6109 69.6527 82.9424 69.1042 82.394C68.5558 81.8455 67.8873 81.4322 67.1515 81.1867L63.2082 79.87C62.7116 79.7033 62.28 79.3848 61.9742 78.9596C61.6684 78.5343 61.5039 78.0238 61.5039 77.5C61.5039 76.9762 61.6684 76.4656 61.9742 76.0404C62.28 75.6152 62.7116 75.2967 63.2082 75.13L67.1515 73.8133C68.6415 73.3133 69.8149 72.1467 70.3115 70.6533L71.6282 66.71C71.7939 66.2124 72.112 65.7795 72.5373 65.4726C72.9626 65.1657 73.4737 65.0004 73.9982 65Z" fill="url(#paint0_linear_2381_2707)"/>
</g>
<defs>
<filter id="filter0_d_2381_2707" x="0" y="0" width="118.004" height="117.994" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="12"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.682353 0 0 0 0 0.447059 0 0 0 0 0.976471 0 0 0 0.4 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2381_2707"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2381_2707" result="shape"/>
</filter>
<linearGradient id="paint0_linear_2381_2707" x1="59.0019" y1="89.9943" x2="59.0019" y2="20" gradientUnits="userSpaceOnUse">
<stop stop-color="#A65EFF"/>
<stop offset="1" stop-color="#EFE3FE"/>
</linearGradient>
</defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -1,7 +1,8 @@
import React, { useState, useEffect, useRef } from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CrawlingResponse, TargetPersona, SellingPoint } from '../../types/api'; import { CrawlingResponse, TargetPersona } from '../../types/api';
import { GeometricChart } from './GeometricChart';
interface AnalysisResultSectionProps { interface AnalysisResultSectionProps {
onBack: () => void; onBack: () => void;
@ -9,200 +10,9 @@ interface AnalysisResultSectionProps {
data: CrawlingResponse; data: CrawlingResponse;
} }
// 레이더 차트 컴포넌트
interface RadarChartProps {
data: SellingPoint[];
size?: number;
}
const RadarChart: React.FC<RadarChartProps> = ({ data, size = 360 }) => {
const [animatedScores, setAnimatedScores] = useState<number[]>(data.map(() => 0));
const [isAnimating, setIsAnimating] = useState(true);
useEffect(() => {
const targetScores = data.map(item => item.score);
const duration = 1500;
const startTime = Date.now();
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeProgress = 1 - Math.pow(1 - progress, 3);
const newScores = targetScores.map(target => target * easeProgress);
setAnimatedScores(newScores);
if (progress < 1) {
requestAnimationFrame(animate);
} else {
setIsAnimating(false);
}
};
requestAnimationFrame(animate);
}, [data]);
const padding = 80;
const center = size / 2 + padding;
const maxRadius = size / 2 - 20;
const levels = 5;
const angleStep = (2 * Math.PI) / data.length;
const getPoint = (index: number, score: number) => {
const angle = angleStep * index - Math.PI / 2;
const radius = (score / 100) * maxRadius;
return {
x: center + radius * Math.cos(angle),
y: center + radius * Math.sin(angle),
};
};
const dataPoints = animatedScores.map((score, i) => getPoint(i, score));
const dataPath = dataPoints.map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`)).join(' ') + ' Z';
const levelPaths = Array.from({ length: levels }, (_, levelIndex) => {
const levelRadius = ((levelIndex + 1) / levels) * maxRadius;
const points = data.map((_, i) => {
const angle = angleStep * i - Math.PI / 2;
return {
x: center + levelRadius * Math.cos(angle),
y: center + levelRadius * Math.sin(angle),
};
});
return points.map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`)).join(' ') + ' Z';
});
const axisLines = data.map((_, i) => {
const angle = angleStep * i - Math.PI / 2;
const endX = center + maxRadius * Math.cos(angle);
const endY = center + maxRadius * Math.sin(angle);
return `M ${center} ${center} L ${endX} ${endY}`;
});
const svgSize = size + padding * 2;
// 순위별로 정렬하여 라벨 위치에 순위 번호 배치
const sortedIndices = [...data]
.map((item, i) => ({ index: i, score: item.score }))
.sort((a, b) => b.score - a.score)
.map((item, rank) => ({ ...item, rank: rank + 1 }));
const rankMap = new Map<number, number>();
sortedIndices.forEach(item => rankMap.set(item.index, item.rank));
return (
<div className="bi2-radar-container">
<svg
width={svgSize}
height={svgSize}
viewBox={`0 0 ${svgSize} ${svgSize}`}
style={{ overflow: 'visible' }}
>
{levelPaths.map((path, i) => (
<path
key={`level-${i}`}
d={path}
fill="none"
stroke="rgba(255,255,255,0.2)"
strokeWidth="1"
/>
))}
{axisLines.map((path, i) => (
<path
key={`axis-${i}`}
d={path}
fill="none"
stroke="rgba(255,255,255,0.2)"
strokeWidth="1"
/>
))}
<path
d={dataPath}
fill="rgba(45, 212, 191, 0.2)"
stroke="#2DD4BF"
strokeWidth="2"
style={{
transition: isAnimating ? 'none' : 'd 0.3s ease-out',
}}
/>
{dataPoints.map((point, i) => (
<circle
key={`point-${i}`}
cx={point.x}
cy={point.y}
r="4"
fill="#2DD4BF"
stroke="#1A1A2E"
strokeWidth="2"
/>
))}
{data.map((item, i) => {
const angle = angleStep * i - Math.PI / 2;
const labelRadius = maxRadius + 40;
const pos = {
x: center + labelRadius * Math.cos(angle),
y: center + labelRadius * Math.sin(angle),
};
const rank = rankMap.get(i) || i + 1;
const isTopThree = rank <= 3;
return (
<g key={`label-${i}`}>
<g transform={`translate(${pos.x}, ${pos.y})`}>
<rect
x="-10"
y="-10"
width="20"
height="20"
rx="4"
fill={isTopThree ? '#94FBE0' : '#206764'}
/>
<text
x="0"
y="0"
textAnchor="middle"
dominantBaseline="central"
style={{
fontSize: '12px',
fontWeight: 600,
fill: isTopThree ? '#002224' : '#94FBE0',
fontFamily: 'Pretendard',
}}
>
{rank}
</text>
<text
x="16"
y="0"
textAnchor="start"
dominantBaseline="central"
style={{
fontSize: '14px',
fontWeight: 400,
fill: '#E5F1F2',
fontFamily: 'Pretendard',
}}
>
{item.korean_category}
</text>
</g>
</g>
);
})}
</svg>
</div>
);
};
const AnalysisResultSection: React.FC<AnalysisResultSectionProps> = ({ onBack, onGenerate, data }) => { const AnalysisResultSection: React.FC<AnalysisResultSectionProps> = ({ onBack, onGenerate, data }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { processed_info, marketing_analysis } = data; const { processed_info, marketing_analysis } = data;
const containerRef = useRef<HTMLDivElement>(null);
const [buttonPosition, setButtonPosition] = useState({ left: 0, width: 0 });
const brandIdentity = marketing_analysis?.brand_identity; const brandIdentity = marketing_analysis?.brand_identity;
const marketPositioning = marketing_analysis?.market_positioning; const marketPositioning = marketing_analysis?.market_positioning;
@ -213,26 +23,6 @@ const AnalysisResultSection: React.FC<AnalysisResultSectionProps> = ({ onBack, o
// 셀링 포인트를 score 내림차순으로 정렬 // 셀링 포인트를 score 내림차순으로 정렬
const sortedSellingPoints = [...sellingPoints].sort((a, b) => b.score - a.score); const sortedSellingPoints = [...sellingPoints].sort((a, b) => b.score - a.score);
useEffect(() => {
const updateButtonPosition = () => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
setButtonPosition({ left: rect.left, width: rect.width });
}
};
updateButtonPosition();
window.addEventListener('resize', updateButtonPosition);
const observer = new MutationObserver(updateButtonPosition);
observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['class'] });
return () => {
window.removeEventListener('resize', updateButtonPosition);
observer.disconnect();
};
}, []);
return ( return (
<div className="bi2-page"> <div className="bi2-page">
{/* Header */} {/* Header */}
@ -253,13 +43,13 @@ const AnalysisResultSection: React.FC<AnalysisResultSectionProps> = ({ onBack, o
<div className="bi2-title-text"> <div className="bi2-title-text">
<h1 className="bi2-main-title">{t('analysis.pageTitle')}</h1> <h1 className="bi2-main-title">{t('analysis.pageTitle')}</h1>
<p className="bi2-subtitle"> <p className="bi2-subtitle">
{t('analysis.pageDescHighlight')}{t('analysis.pageDescBefore')}{processed_info?.customer_name || t('analysis.defaultBrandName')}{t('analysis.pageDescAfter')} <span style={{ color: 'var(--Color-mint-500, #94FBE0)' }}>{t('analysis.pageDescHighlight')}</span>{t('analysis.pageDescBefore')}{processed_info?.customer_name || t('analysis.defaultBrandName')}{t('analysis.pageDescAfter')}
</p> </p>
</div> </div>
</div> </div>
{/* Main Content Container */} {/* Main Content Container */}
<div ref={containerRef} className="bi2-main-container"> <div className="bi2-main-container">
{/* 매장명 & 주소 */} {/* 매장명 & 주소 */}
<div className="bi2-store-header"> <div className="bi2-store-header">
<h2 className="bi2-store-name">{processed_info?.customer_name || t('analysis.brandNameFallback')}</h2> <h2 className="bi2-store-name">{processed_info?.customer_name || t('analysis.brandNameFallback')}</h2>
@ -298,7 +88,7 @@ const AnalysisResultSection: React.FC<AnalysisResultSectionProps> = ({ onBack, o
{/* 레이더 차트 */} {/* 레이더 차트 */}
<div className="bi2-selling-chart"> <div className="bi2-selling-chart">
{sellingPoints.length > 0 && ( {sellingPoints.length > 0 && (
<RadarChart data={sellingPoints} size={360} /> <GeometricChart data={sellingPoints} />
)} )}
</div> </div>
{/* 셀링 포인트 리스트 */} {/* 셀링 포인트 리스트 */}
@ -369,13 +159,10 @@ const AnalysisResultSection: React.FC<AnalysisResultSectionProps> = ({ onBack, o
</div> </div>
{/* 콘텐츠 생성 버튼 */} {/* 콘텐츠 생성 버튼 */}
<div <div className="bi2-bottom-button-container">
className="fixed bottom-8 flex justify-center z-50 pointer-events-none"
style={{ left: buttonPosition.left, width: buttonPosition.width }}
>
<button <button
onClick={onGenerate} onClick={onGenerate}
className="pointer-events-auto bg-brand-purple hover:bg-brand-purpleHover text-white font-bold py-3 px-12 rounded-full shadow-2xl shadow-brand-purple/40 transform hover:scale-105 transition-all duration-300 flex items-center gap-2" className="bi2-generate-btn"
> >
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5"> <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">
<path d="M12 2l2.4 6.8L22 12l-7.6 3.2L12 22l-2.4-6.8L2 12l7.6-3.2L12 2z" /> <path d="M12 2l2.4 6.8L22 12l-7.6 3.2L12 22l-2.4-6.8L2 12l7.6-3.2L12 2z" />

View File

@ -1,149 +1,192 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { SellingPoint } from '../../types/api';
export interface USP {
label: string;
subLabel: string;
score: number;
description: string;
}
interface GeometricChartProps { interface GeometricChartProps {
data: USP[]; data: SellingPoint[];
} }
export const GeometricChart: React.FC<GeometricChartProps> = ({ data }) => { export const GeometricChart: React.FC<GeometricChartProps> = ({ data }) => {
const size = 500; const [animatedScores, setAnimatedScores] = useState<number[]>(data.map(() => 0));
useEffect(() => {
const targetScores = data.map(item => item.score);
const duration = 1500;
const startTime = Date.now();
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeProgress = 1 - Math.pow(1 - progress, 3);
setAnimatedScores(targetScores.map(t => t * easeProgress));
if (progress < 1) requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}, [data]);
const size = 560;
const center = size / 2; const center = size / 2;
const radius = 110; const maxRadius = 160;
const sides = data.length; const sides = data.length;
const levels = 5;
const angleStep = (Math.PI * 2) / sides;
const accentColor = "#94FBE0"; const getAngle = (i: number) => angleStep * i - Math.PI / 2;
const getPoint = (i: number, r: number) => ({
const getPoints = (r: number) => { x: center + r * Math.cos(getAngle(i)),
return data.map((_, i) => { y: center + r * Math.sin(getAngle(i)),
const angle = (Math.PI * 2 * i) / sides - Math.PI / 2;
const x = center + r * Math.cos(angle);
const y = center + r * Math.sin(angle);
return `${x},${y}`;
}).join(' ');
};
const getDataPoints = () => {
return data.map((item, i) => {
const normalizedScore = item.score / 100;
const r = radius * normalizedScore;
const angle = (Math.PI * 2 * i) / sides - Math.PI / 2;
const x = center + r * Math.cos(angle);
const y = center + r * Math.sin(angle);
return `${x},${y}`;
}).join(' ');
};
const labelRadius = radius + 55;
const labels = data.map((item, i) => {
const angle = (Math.PI * 2 * i) / sides - Math.PI / 2;
const x = center + labelRadius * Math.cos(angle);
const y = center + labelRadius * Math.sin(angle);
let anchor: 'start' | 'middle' | 'end' = 'middle';
if (x < center - 20) anchor = 'end';
if (x > center + 20) anchor = 'start';
return { x, y, text: item.label, sub: item.subLabel, anchor, score: item.score };
}); });
// 순위 매핑
const sortedIndices = [...data]
.map((item, i) => ({ index: i, score: item.score }))
.sort((a, b) => b.score - a.score)
.map((item, rank) => ({ ...item, rank: rank + 1 }));
const rankMap = new Map<number, number>();
sortedIndices.forEach(item => rankMap.set(item.index, item.rank));
// 레벨 폴리곤
const levelPolygons = Array.from({ length: levels }, (_, li) => {
const r = ((li + 1) / levels) * maxRadius;
return data.map((_, i) => {
const p = getPoint(i, r);
return `${p.x},${p.y}`;
}).join(' ');
});
// 데이터 폴리곤
const dataPolygon = animatedScores.map((score, i) => {
const r = (score / 100) * maxRadius;
const p = getPoint(i, r);
return `${p.x},${p.y}`;
}).join(' ');
// 데이터 포인트
const dataPoints = animatedScores.map((score, i) => {
const r = (score / 100) * maxRadius;
return getPoint(i, r);
});
const labelRadius = maxRadius + 22;
return ( return (
<div className="flex flex-col items-center justify-center py-2 relative w-full h-full"> <div className="bi2-radar-container">
<svg viewBox={`0 0 ${size} ${size}`} className="w-full h-auto max-w-[500px]" style={{ overflow: 'visible' }}> <svg
viewBox={`0 0 ${size} ${size}`}
style={{ overflow: 'visible', width: '440px', height: '440px' }}
>
<defs> <defs>
<radialGradient id="polyGradient" cx="50%" cy="50%" r="50%" fx="50%" fy="50%"> <filter id="radar-glow" x="-20%" y="-20%" width="140%" height="140%">
<stop offset="0%" stopColor={accentColor} stopOpacity="0.3" /> <feGaussianBlur stdDeviation="4" result="coloredBlur" />
<stop offset="100%" stopColor={accentColor} stopOpacity="0.05" /> <feMerge>
</radialGradient> <feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<filter id="badge-glow" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="0" stdDeviation="4" floodColor="#94FBE0" floodOpacity="0.4" />
</filter>
</defs> </defs>
{[1, 0.75, 0.5, 0.25].map((scale, i) => ( {/* 그리드 레벨 */}
{levelPolygons.map((points, i) => (
<polygon <polygon
key={i} key={`level-${i}`}
points={getPoints(radius * scale)} points={points}
fill="none" fill="none"
stroke={accentColor} stroke="rgba(255,255,255,0.2)"
strokeOpacity={0.25 - (0.02 * i)}
strokeWidth="1" strokeWidth="1"
strokeDasharray={i === 0 ? "0" : "4 2"}
/> />
))} ))}
{/* 축 선 */}
{data.map((_, i) => { {data.map((_, i) => {
const angle = (Math.PI * 2 * i) / sides - Math.PI / 2; const p = getPoint(i, maxRadius);
const x = center + radius * Math.cos(angle);
const y = center + radius * Math.sin(angle);
return ( return (
<line <line
key={i} key={`axis-${i}`}
x1={center} x1={center}
y1={center} y1={center}
x2={x} x2={p.x}
y2={y} y2={p.y}
stroke={accentColor} stroke="rgba(255,255,255,0.2)"
strokeOpacity="0.25"
strokeWidth="1" strokeWidth="1"
/> />
); );
})} })}
{/* 데이터 폴리곤 */}
<polygon <polygon
points={getDataPoints()} points={dataPolygon}
fill="url(#polyGradient)" fill="rgba(45, 212, 191, 0.15)"
stroke={accentColor} stroke="#2DD4BF"
strokeWidth="2.5" strokeWidth="2"
strokeLinejoin="round" strokeLinejoin="round"
filter="url(#radar-glow)"
/> />
{data.map((item, i) => { {/* 데이터 포인트 */}
const normalizedScore = item.score / 100; {dataPoints.map((point, i) => (
const r = radius * normalizedScore; <circle
const angle = (Math.PI * 2 * i) / sides - Math.PI / 2; key={`point-${i}`}
const x = center + r * Math.cos(angle); cx={point.x}
const y = center + r * Math.sin(angle); cy={point.y}
const isHigh = item.score >= 90; r="4"
return ( fill="#2DD4BF"
<g key={i}> stroke="#1A1A2E"
{isHigh && ( strokeWidth="2"
<circle cx={x} cy={y} r="10" fill={accentColor} fillOpacity="0.4" /> />
)} ))}
<circle cx={x} cy={y} r={isHigh ? 5 : 4} fill="#fff" />
</g> {/* 라벨 */}
) {data.map((item, i) => {
})} const angle = getAngle(i);
const pos = getPoint(i, labelRadius);
const rank = rankMap.get(i) || i + 1;
const isTopThree = rank <= 3;
const isLeftSide = Math.cos(angle) < -0.1;
const textX = isLeftSide ? -14 : 14;
const textAnchor: 'start' | 'end' = isLeftSide ? 'end' : 'start';
{labels.map((l, i) => {
const isHigh = l.score >= 90;
return ( return (
<g key={i}> <g key={`label-${i}`}>
<text <g transform={`translate(${pos.x}, ${pos.y})`}>
x={l.x} {isLeftSide && (
y={l.y - 7} <text
textAnchor={l.anchor} x={textX}
fill={isHigh ? "#fff" : "#e2e8f0"} y="0"
fontSize={isHigh ? "14" : "12"} textAnchor={textAnchor}
fontWeight={isHigh ? "700" : "600"} dominantBaseline="central"
className="tracking-tight" className="bi2-radar-label-text"
style={{ fontFamily: "sans-serif" }} style={{ fontSize: '20px', fontWeight: 600, fill: '#E5F1F2', fontFamily: 'Pretendard' }}
> >
{l.text} {item.korean_category}
</text> </text>
<text )}
x={l.x} <g filter={isTopThree ? 'url(#badge-glow)' : undefined}>
y={l.y + 9} <rect x="-12.5" y="-12.5" width="25" height="25" rx="4" fill={isTopThree ? '#94FBE0' : '#206764'} />
textAnchor={l.anchor} <text
fill={isHigh ? accentColor : "#94a3b8"} x="0" y="0"
fontSize={isHigh ? "11" : "10"} textAnchor="middle"
fontWeight={isHigh ? "600" : "400"} dominantBaseline="central"
className="uppercase tracking-wider" style={{ fontSize: '16px', fontWeight: 600, fill: isTopThree ? '#002224' : '#94FBE0', fontFamily: 'Pretendard' }}
> >
{l.sub} {rank}
</text> </text>
</g>
{!isLeftSide && (
<text
x={textX}
y="0"
textAnchor={textAnchor}
dominantBaseline="central"
className="bi2-radar-label-text"
style={{ fontSize: '20px', fontWeight: 600, fill: '#E5F1F2', fontFamily: 'Pretendard' }}
>
{item.korean_category}
</text>
)}
</g>
</g> </g>
); );
})} })}

View File

@ -518,32 +518,8 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
</div> </div>
</div> </div>
{/* Generate Button */} {/* Generate Button / Status Message (교체) */}
<button {isGenerating && statusMessage ? (
onClick={handleGenerateMusic}
disabled={isGenerating}
className={`btn-generate-sound ${isGenerating ? 'disabled' : ''}`}
>
{isGenerating ? (
<>
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"/>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg>
{t('soundStudio.generating')}
</>
) : (
t('soundStudio.generateButton')
)}
</button>
{errorMessage && (
<div className="error-message-new">
{errorMessage}
</div>
)}
{isGenerating && statusMessage && (
<div className="status-message-new"> <div className="status-message-new">
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24"> <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"/> <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"/>
@ -551,6 +527,20 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
</svg> </svg>
{statusMessage} {statusMessage}
</div> </div>
) : (
<button
onClick={handleGenerateMusic}
disabled={isGenerating}
className={`btn-generate-sound ${isGenerating ? 'disabled' : ''}`}
>
{t('soundStudio.generateButton')}
</button>
)}
{errorMessage && (
<div className="error-message-new">
{errorMessage}
</div>
)} )}
</div> </div>