From 4edd5897771e8847bd39a93d2e5353a9b1fa505f Mon Sep 17 00:00:00 2001 From: ZinYan Date: Sun, 17 Aug 2025 23:22:00 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[bug]=20=EC=B6=94=EC=B2=9C=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=88=98=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/main/main.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/templates/main/main.html b/templates/main/main.html index aa35dbc..a0aad0c 100644 --- a/templates/main/main.html +++ b/templates/main/main.html @@ -111,9 +111,11 @@

๐ŸŽฏ ์˜ค๋Š˜์˜ ์ถ”์ฒœ

{{ recommended_product.product_name }}

{% if recommended_product.is_user_liked %} ๐Ÿ’– ๋‚ด๊ฐ€ ์ฐœํ•œ ์ œํ’ˆ | {{ - recommended_product.product_brand }} {% else %} {{ - recommended_product.product_brand }} ๐Ÿ’œ | {{ - recommended_product.like_count }}๋ช…์ด ์ฐœํ•จ {% endif %} + recommended_product.product_brand }} + {% else %} + {{recommended_product.product_brand }} ๐Ÿ’œ | + {{recommended_product.like_count }}๋ช…์ด ์ฐœํ•จ + {% endif %}

From 0fd3cc0037356b5b1ceb14f2993ae9779e700034 Mon Sep 17 00:00:00 2001 From: ZinYan Date: Sun, 17 Aug 2025 23:24:05 +0900 Subject: [PATCH 2/5] [deploy] collect static --- staticfiles/css/main/main.css | 294 ++++++++++++- .../css/personalcolor/personalcolor.css | 261 ++++++++++++ staticfiles/css/products/product_ranking.css | 248 +++++------ staticfiles/css/products/user_ratings.css | 398 ++++++++++++++++++ .../css/recommendation/color_matrix_style.css | 8 +- staticfiles/css/style.css | 123 ++++-- staticfiles/css/upload/upload.css | 300 ++++++++++++- staticfiles/js/main/main.js | 68 ++- staticfiles/js/products/ranking_filter.js | 42 ++ .../js/recommendation/color_analyzer.js | 67 ++- 10 files changed, 1611 insertions(+), 198 deletions(-) create mode 100644 staticfiles/css/personalcolor/personalcolor.css create mode 100644 staticfiles/css/products/user_ratings.css create mode 100644 staticfiles/js/products/ranking_filter.js diff --git a/staticfiles/css/main/main.css b/staticfiles/css/main/main.css index 273b15c..9cc056d 100644 --- a/staticfiles/css/main/main.css +++ b/staticfiles/css/main/main.css @@ -2,11 +2,10 @@ display: flex; flex-direction: column; align-items: center; - padding: 50px 20px; + padding: 30px 20px; text-align: center; - width: 1020px; - margin-left: 200px; - + max-width: 1200px; + margin: 0 auto; } .main-container h3 { @@ -14,7 +13,7 @@ } .main-container p { - color: mediumpurple; + color: #664589; margin-bottom: 50px; } @@ -22,12 +21,12 @@ .personal-color-banner { background: linear-gradient(135deg, #fce4ec 0%, #f3e5f5 50%, #fff8e1 100%); border-radius: 25px; - padding: 40px; - margin-bottom: 50px; + padding: 30px; + margin-bottom: 40px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); text-align: center; - max-width: 1050px; - width: 900px; + max-width: 800px; + width: 90%; } .banner-content h2 { @@ -70,8 +69,9 @@ grid-template-columns: repeat(2, 1fr); gap: 25px; width: 90%; - max-width: 1200px; + max-width: 900px; margin: 0 auto 50px; + justify-content: center; } .feature-card { @@ -837,4 +837,276 @@ to { opacity: 1; } -} \ No newline at end of file +} + +/* ํผ์Šค๋„ ์ปฌ๋Ÿฌ ํŽ˜์ด์ง€ ์Šคํƒ€์ผ */ + /* ===== Personal Color Onboarding (scoped with .pc-*) ===== */ + :root { + --pc-brand: #6f4cff; + --pc-brand-600: #5b3ce6; + --pc-line: #ece8ff; + --pc-muted: #6b7280; + --pc-card: #fff; + --pc-shadow: 0 10px 30px rgba(24,19,56,.12); + --pc-radius: 16px; + --pc-radius-lg: 22px; + + } + .pc-section { padding: 10px 0; } + .pc-container { max-width: 1120px; margin: 0 auto; padding: 0 20px; } + .pc-title-xl { font-size: clamp(28px,3.6vw,40px); line-height: 1.15; margin: 150px 0 70px; } + .pc-subtitle { color: var(--pc-muted); font-size: clamp(15px,1.9vw,18px); } + .pc-eyebrow { font-size: 12px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; color: #472ec7; } + + .pc-card { background: var(--pc-card); border: 1px solid var(--pc-line); border-radius: var(--pc-radius); box-shadow: var(--pc-shadow); padding: 20px; } + .pc-grid { display: grid; gap: 18px; } + .pc-g2 { grid-template-columns: repeat(2,1fr); } + .pc-g3 { grid-template-columns: repeat(3,1fr); } + .pc-g4 { grid-template-columns: repeat(4,1fr); } + @media (max-width: 1000px) { .pc-g4{grid-template-columns:repeat(2,1fr);} } + @media (max-width: 820px) { .pc-g2,.pc-g3,.pc-g4{grid-template-columns:1fr;} } + + .pc-hero { + border: 1px solid var(--pc-line); + border-radius: var(--pc-radius-lg); + box-shadow: var(--pc-shadow); + padding: clamp(20px,4vw,42px); + display: grid; grid-template-columns: 1.25fr .9fr; gap: clamp(18px,4vw,36px); + +} + +.pc-section { + padding-bottom: 25px; +} + +.pc-container { + max-width: 1120px; + margin: 0 auto; + padding: 0 20px; +} + +.pc-title-xl { + font-size: clamp(28px, 3.6vw, 40px); + line-height: 1.15; + margin: 95px 0 12px; +} + +.pc-subtitle { + color: var(--pc-muted); + font-size: clamp(15px, 1.9vw, 18px); +} + +.pc-eyebrow { + font-size: 12px; + font-weight: 800; + letter-spacing: .16em; + text-transform: uppercase; + color: #472ec7; +} + +.pc-card { + background: var(--pc-card); + border: 1px solid var(--pc-line); + border-radius: var(--pc-radius); + box-shadow: var(--pc-shadow); + padding: 20px; +} + +.pc-grid { + display: grid; + gap: 18px; +} + +.pc-g2 { + grid-template-columns: repeat(2, 1fr); +} + +.pc-g3 { + grid-template-columns: repeat(3, 1fr); +} + +.pc-g4 { + grid-template-columns: repeat(4, 1fr); +} + +@media (max-width: 1000px) { + .pc-g4 { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 820px) { + + .pc-g2, + .pc-g3, + .pc-g4 { + grid-template-columns: 1fr; + } +} + +.pc-hero { + border-radius: var(--pc-radius-lg); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); + border: 1px solid #f0f0f0; + padding: clamp(20px, 4vw, 42px); + display: grid; + grid-template-columns: 1.25fr .9fr; + gap: clamp(18px, 4vw, 36px); + + background: #fff; + } + @media (max-width: 900px) { .pc-hero{grid-template-columns:1fr;} } + + .pc-ring { + aspect-ratio: 1/1; border-radius: 24px; position: relative; overflow: hidden; + background: conic-gradient( + from 0deg, + #ffad92 0 25%, #cbb8ff 0 50%, #d9a46b 0 75%, #9bd3ff 0 100% + ); + border: 1px solid var(--pc-line); + } + .pc-ring::after { + content: ""; position: absolute; inset: 14%; background: radial-gradient(circle at 50% 50%, #fff 0 60%, transparent 60%); + border-radius: 22px; box-shadow: inset 0 0 0 1px var(--pc-line); + } + + .pc-badge { display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:999px; font-size:12px; font-weight:800; border:1px solid var(--pc-line); } + .pc-badge.warm { background:#fff7ed; color:#b45309; border-color:#fed7aa; } + .pc-badge.cool { background:#eef2ff; color:#3730a3; border-color:#c7d2fe; } + + .pc-chips { display:flex; flex-wrap:wrap; gap:10px; margin-top:10px; } + .pc-chip { --c:#ddd; --label:""; position:relative; width:42px; height:42px; border-radius:10px; background:var(--c); + border:1px solid rgba(0,0,0,.06); box-shadow: inset 0 1px 0 rgba(255,255,255,.35), 0 2px 8px rgba(16,16,24,.12); + } + .pc-chip::after { content:var(--label); position:absolute; bottom:-18px; left:50%; transform:translateX(-50%); font-size:10px; color:var(--pc-muted); white-space:nowrap; } + + .pc-list { margin:10px 0 0 0; padding:0 0 0 18px; color:var(--pc-muted); } + .pc-list li { margin:6px 0; } + + .pc-cta { border: 1px dashed #dcd3ff; background: linear-gradient(180deg, #faf9ff 0%, #fff 100%); border-radius: var(--pc-radius); + padding: 24px; display: grid; grid-template-columns: 1.3fr .7fr; gap: 16px; align-items:center; } + @media (max-width: 900px) { .pc-cta{grid-template-columns:1fr;} } + + .pc-btn { display:inline-flex; align-items:center; gap:8px; padding:10px 14px; border-radius:12px; font-weight:800; font-size:14px; border:1px solid transparent; cursor:pointer; text-decoration:none; } + .pc-btn.primary { background: var(--pc-brand); color:#fff; box-shadow:0 8px 18px rgba(111,76,255,.28); } + .pc-btn.primary:hover { background: var(--pc-brand-600); } + .pc-btn.ghost { background:#fff; border-color:var(--pc-line); color:#111; } + .pc-btn.ghost:hover { background:#fbfaff; border-color:#d9d3ff; } + + .pc-mood-tag { border: 1px solid var(--pc-line); border-radius: 999px; padding: 8px 12px; font-weight: 800; font-size: 13px; background:#fff; } + .pc-mood-dot { width:10px; height:10px; border-radius:999px; display:inline-block; border:1px solid rgba(0,0,0,.08); vertical-align:middle; margin-right:8px; } + + /* simple reveal */ + .pc-reveal { opacity:0; transform: translateY(10px); transition: opacity .5s ease, transform .6s ease; } + .pc-reveal.on { opacity:1; transform: translateY(0); } + +/* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */ +@media (max-width: 768px) { + .main-container { + padding: 20px 15px; + } + + .personal-color-banner { + padding: 25px; + margin-bottom: 30px; + } + + .main-features { + grid-template-columns: 1fr; + gap: 20px; + width: 95%; + } + + .feature-card { + padding: 25px; + } +} + +@media (max-width: 480px) { + .main-container { + padding: 15px 10px; + } + + .personal-color-banner { + padding: 20px; + margin-bottom: 25px; + } + + .main-features { + width: 98%; + gap: 15px; + } + + .feature-card { + padding: 20px; + } +} + + +.matrix-section { + flex: 1; + min-width: 400px; +} + +.color-matrix-container { + position: relative; + width: 100%; + height: 400px; + background: linear-gradient(to bottom right, + #f8f0ff, + #e0efff, + #ffe0f0, + #fff8e0); + border-radius: 15px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + overflow: visible; + border: 1px solid #ddd; + min-width: 400px; +} + + + + +.axis-label { + position: absolute; + font-weight: bold; + color: #777; + font-size: 0.9em; + z-index: 5; +} + +.top-label { + top: 10px; + left: 50%; + transform: translateX(-50%); +} + +.bottom-label { + bottom: 10px; + left: 50%; + transform: translateX(-50%); +} + +.left-label { + left: 10px; + top: 50%; + transform: translateY(-50%) rotate(-90deg); +} + +.right-label { + right: 10px; + top: 50%; + transform: translateY(-50%) rotate(90deg); +} + +.makeup-products-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2; + min-width: 400px; + border-radius: 15px; +} + diff --git a/staticfiles/css/personalcolor/personalcolor.css b/staticfiles/css/personalcolor/personalcolor.css new file mode 100644 index 0000000..0379b56 --- /dev/null +++ b/staticfiles/css/personalcolor/personalcolor.css @@ -0,0 +1,261 @@ +:root { + --pc-brand: #6f4cff; + --pc-brand-600: #5b3ce6; + --pc-line: #ece8ff; + --pc-muted: #6b7280; + --pc-card: #fff; + --pc-shadow: 0 10px 30px rgba(24, 19, 56, .12); + --pc-radius: 16px; + --pc-radius-lg: 22px; +} + +.pc-section { + padding: 10px 0; +} + +.pc-container { + max-width: 1120px; + margin: 0 auto; + padding: 0 20px; +} + +.pc-title-xl h2 { + font-size: clamp(28px, 3.6vw, 40px); + line-height: 1.15; + margin: 6px 0 12px; + color: #664589; +} + +.pc-subtitle { + color: var(--pc-muted); + font-size: clamp(15px, 1.9vw, 18px); +} + +.pc-eyebrow { + font-size: 12px; + font-weight: 800; + letter-spacing: .16em; + text-transform: uppercase; + color: #472ec7; +} + +.pc-card { + background: var(--pc-card); + border: 1px solid var(--pc-line); + border-radius: var(--pc-radius); + box-shadow: var(--pc-shadow); + padding: 20px; +} + +.pc-grid { + display: grid; + gap: 18px; +} + +.pc-g2 { + grid-template-columns: repeat(2, 1fr); +} + +.pc-g3 { + grid-template-columns: repeat(3, 1fr); +} + +.pc-g4 { + grid-template-columns: repeat(4, 1fr); +} + +@media (max-width: 1000px) { + .pc-g4 { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 820px) { + + .pc-g2, + .pc-g3, + .pc-g4 { + grid-template-columns: 1fr; + } +} + +.pc-hero { + border: 1px solid var(--pc-line); + border-radius: var(--pc-radius-lg); + box-shadow: var(--pc-shadow); + padding: clamp(20px, 4vw, 42px); + display: grid; + grid-template-columns: 1.25fr .9fr; + gap: clamp(18px, 4vw, 36px); + background: #fff; +} + +@media (max-width: 900px) { + .pc-hero { + grid-template-columns: 1fr; + } +} + +.pc-ring { + aspect-ratio: 1/1; + border-radius: 24px; + position: relative; + overflow: hidden; + background: conic-gradient(from 0deg, + #ffad92 0 25%, #cbb8ff 0 50%, #d9a46b 0 75%, #9bd3ff 0 100%); + border: 1px solid var(--pc-line); +} + +.pc-ring::after { + content: ""; + position: absolute; + inset: 14%; + background: radial-gradient(circle at 50% 50%, #fff 0 60%, transparent 60%); + border-radius: 22px; + box-shadow: inset 0 0 0 1px var(--pc-line); +} + +.pc-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 800; + border: 1px solid var(--pc-line); +} + +.pc-badge.warm { + background: #fff7ed; + color: #b45309; + border-color: #fed7aa; +} + +.pc-badge.cool { + background: #eef2ff; + color: #3730a3; + border-color: #c7d2fe; +} + +.pc-chips { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 10px; +} + +.pc-chip { + --c: #ddd; + --label: ""; + position: relative; + width: 42px; + height: 42px; + border-radius: 10px; + background: var(--c); + border: 1px solid rgba(0, 0, 0, .06); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .35), 0 2px 8px rgba(16, 16, 24, .12); +} + +.pc-chip::after { + content: var(--label); + position: absolute; + bottom: -18px; + left: 50%; + transform: translateX(-50%); + font-size: 10px; + color: var(--pc-muted); + white-space: nowrap; +} + +.pc-list { + margin: 10px 0 0 0; + padding: 0 0 0 18px; + color: var(--pc-muted); +} + +.pc-list li { + margin: 6px 0; +} + +.pc-cta { + border: 1px dashed #dcd3ff; + background: linear-gradient(180deg, #faf9ff 0%, #fff 100%); + border-radius: var(--pc-radius); + padding: 24px; + display: grid; + grid-template-columns: 1.3fr .7fr; + gap: 16px; + align-items: center; +} + +@media (max-width: 900px) { + .pc-cta { + grid-template-columns: 1fr; + } +} + +.pc-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + border-radius: 12px; + font-weight: 800; + font-size: 14px; + border: 1px solid transparent; + cursor: pointer; + text-decoration: none; +} + +.pc-btn.primary { + background: var(--pc-brand); + color: #fff; + box-shadow: 0 8px 18px rgba(111, 76, 255, .28); +} + +.pc-btn.primary:hover { + background: var(--pc-brand-600); +} + +.pc-btn.ghost { + background: #fff; + border-color: var(--pc-line); + color: #111; +} + +.pc-btn.ghost:hover { + background: #fbfaff; + border-color: #d9d3ff; +} + +.pc-mood-tag { + border: 1px solid var(--pc-line); + border-radius: 999px; + padding: 8px 12px; + font-weight: 800; + font-size: 13px; + background: #fff; +} + +.pc-mood-dot { + width: 10px; + height: 10px; + border-radius: 999px; + display: inline-block; + border: 1px solid rgba(0, 0, 0, .08); + vertical-align: middle; + margin-right: 8px; +} + +/* simple reveal */ +.pc-reveal { + opacity: 0; + transform: translateY(10px); + transition: opacity .5s ease, transform .6s ease; +} + +.pc-reveal.on { + opacity: 1; + transform: translateY(0); +} \ No newline at end of file diff --git a/staticfiles/css/products/product_ranking.css b/staticfiles/css/products/product_ranking.css index 9bb78ee..9a14351 100644 --- a/staticfiles/css/products/product_ranking.css +++ b/staticfiles/css/products/product_ranking.css @@ -1,9 +1,11 @@ +/* ====== ๋žญํ‚น ์ปจํ…Œ์ด๋„ˆ ====== */ .ranking-container { max-width: 1200px; margin: 0 auto; padding: 40px 20px; } +/* ====== ํ—ค๋” ====== */ .ranking-header { text-align: center; margin-bottom: 50px; @@ -24,10 +26,37 @@ margin-bottom: 20px; } +/* ====== ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ ====== */ +.category-filter { + margin-bottom: 20px; +} + +.category-filter a { + display: inline-block; + margin: 0 8px 10px 0; + padding: 6px 16px; + border-radius: 25px; + background-color: #f0f0f0; + color: #333; + text-decoration: none; + font-weight: 500; + transition: all 0.2s ease; +} + +.category-filter a:hover { background-color: #ddd; } + +.category-filter a.active { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + font-weight: 600; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); +} + +/* ====== ๋žญํ‚น ํ†ต๊ณ„ ====== */ .ranking-stats { background: linear-gradient(135deg, - rgba(255, 107, 107, 0.1), - rgba(147, 112, 219, 0.1)); + rgba(255, 107, 107, 0.1), + rgba(147, 112, 219, 0.1)); padding: 15px 25px; border-radius: 25px; display: inline-block; @@ -35,107 +64,76 @@ color: #333; } +/* ====== ๊ทธ๋ฆฌ๋“œ ====== */ .ranking-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); - gap: 25px; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 20px; margin-bottom: 40px; } +/* ====== ์นด๋“œ ====== */ .ranking-card { background: white; - border-radius: 20px; - padding: 25px; + border-radius: 18px; + padding: 18px; /* ๊ธฐ์กด 20px โ†’ 18px */ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); transition: all 0.3s ease; border: 1px solid #eee; position: relative; display: flex; align-items: center; - gap: 20px; + gap: 12px; /* ๊ธฐ์กด 15px โ†’ 12px */ } .ranking-card:hover { - transform: translateY(-8px); - box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15); + transform: translateY(-6px); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15); cursor: pointer; } .ranking-card.top-3 { border: 2px solid; background: linear-gradient(135deg, - rgba(255, 215, 0, 0.05), - rgba(255, 255, 255, 1)); + rgba(255, 215, 0, 0.05), + rgba(255, 255, 255, 1)); } -.ranking-card.rank-1 { - border-color: #ffd700; - box-shadow: 0 8px 25px rgba(255, 215, 0, 0.3); -} - -.ranking-card.rank-2 { - border-color: #c0c0c0; - box-shadow: 0 8px 25px rgba(192, 192, 192, 0.3); -} - -.ranking-card.rank-3 { - border-color: #cd7f32; - box-shadow: 0 8px 25px rgba(205, 127, 50, 0.3); -} +.ranking-card.rank-1 { border-color: #ffd700; box-shadow: 0 8px 25px rgba(255, 215, 0, 0.3); } +.ranking-card.rank-2 { border-color: #c0c0c0; box-shadow: 0 8px 25px rgba(192, 192, 192, 0.3); } +.ranking-card.rank-3 { border-color: #cd7f32; box-shadow: 0 8px 25px rgba(205, 127, 50, 0.3); } +/* ====== ๋žญํ‚น ๋ฒˆํ˜ธ ====== */ .ranking-number { - width: 50px; - height: 50px; + width: 42px; /* ๊ธฐ์กด 45px โ†’ 42px */ + height: 42px; /* ๊ธฐ์กด 45px โ†’ 42px */ border-radius: 50%; display: flex; align-items: center; justify-content: center; - font-size: 1.5em; + font-size: 1.35em; font-weight: bold; color: white; flex-shrink: 0; } -.rank-1 .ranking-number { - background: linear-gradient(135deg, #ffd700, #ffed4e); - box-shadow: 0 4px 15px rgba(255, 215, 0, 0.4); -} - -.rank-2 .ranking-number { - background: linear-gradient(135deg, #c0c0c0, #e8e8e8); - box-shadow: 0 4px 15px rgba(192, 192, 192, 0.4); -} - -.rank-3 .ranking-number { - background: linear-gradient(135deg, #cd7f32, #daa520); - box-shadow: 0 4px 15px rgba(205, 127, 50, 0.4); -} - -.ranking-number.other { - background: linear-gradient(135deg, #667eea, #764ba2); - box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); -} +.rank-1 .ranking-number { background: linear-gradient(135deg, #ffd700, #ffed4e); box-shadow: 0 4px 15px rgba(255, 215, 0, 0.4); } +.rank-2 .ranking-number { background: linear-gradient(135deg, #c0c0c0, #e8e8e8); box-shadow: 0 4px 15px rgba(192, 192, 192, 0.4); } +.rank-3 .ranking-number { background: linear-gradient(135deg, #cd7f32, #daa520); box-shadow: 0 4px 15px rgba(205, 127, 50, 0.4); } +.ranking-number.other { background: linear-gradient(135deg, #667eea, #764ba2); box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); } +/* ====== ์ด๋ฏธ์ง€ ====== */ .product-image { - width: 80px; - height: 80px; + width: 60px; /* ๊ธฐ์กด 70px โ†’ 60px */ + height: 60px; /* ๊ธฐ์กด 70px โ†’ 60px */ object-fit: cover; - border-radius: 15px; + border-radius: 12px; flex-shrink: 0; } -.product-details { - flex: 1; - min-width: 0; -} - -.product-brand { - font-size: 0.9em; - color: #888; - margin-bottom: 5px; - font-weight: 500; -} - +/* ====== ์ œํ’ˆ ์ •๋ณด ====== */ +.product-details { flex: 1; min-width: 0; } +.product-brand { font-size: 0.9em; color: #888; margin-bottom: 5px; font-weight: 500; } .product-name { font-size: 1.1em; font-weight: bold; @@ -147,105 +145,55 @@ -webkit-box-orient: vertical; overflow: hidden; } - -.product-price { - font-size: 1em; - color: #ff6699; - font-weight: 600; - margin-bottom: 10px; -} - +.product-price { font-size: 1em; color: #ff6699; font-weight: 600; margin-bottom: 10px; } .like-info { - display: flex; - align-items: center; - gap: 8px; + display: flex; align-items: center; gap: 8px; background: rgba(255, 107, 107, 0.1); - padding: 8px 12px; - border-radius: 20px; - font-size: 0.9em; - font-weight: 600; - color: #ff6b6b; + padding: 8px 12px; border-radius: 20px; + font-size: 0.9em; font-weight: 600; color: #ff6b6b; } -.crown-icon { - position: absolute; - top: -10px; - right: -10px; - font-size: 2em; - transform: rotate(25deg); -} +/* ====== ์™•๊ด€ ====== */ +.crown-icon { position: absolute; top: -10px; right: -10px; font-size: 1.7em; transform: rotate(25deg); } +.rank-1 .crown-icon { color: #ffd700; text-shadow: 0 2px 8px rgba(255, 215, 0, 0.5); } -.rank-1 .crown-icon { - color: #ffd700; - text-shadow: 0 2px 8px rgba(255, 215, 0, 0.5); -} +/* ====== ๋นˆ ์ƒํƒœ ====== */ +.empty-state { text-align: center; padding: 80px 20px; color: #666; } +.empty-state .icon { font-size: 4em; margin-bottom: 20px; opacity: 0.3; } +.empty-state h2 { font-size: 1.8em; margin-bottom: 15px; color: #333; } +.empty-state p { font-size: 1.1em; margin-bottom: 30px; } -.empty-state { - text-align: center; - padding: 80px 20px; - color: #666; -} - -.empty-state .icon { - font-size: 4em; - margin-bottom: 20px; - opacity: 0.3; -} - -.empty-state h2 { - font-size: 1.8em; - margin-bottom: 15px; - color: #333; +/* ====== ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ ====== */ +.back-btn { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; border: none; + padding: 12px 25px; border-radius: 25px; + cursor: pointer; font-size: 1em; font-weight: 600; + transition: all 0.3s ease; text-decoration: none; display: inline-block; margin-top: 20px; } +.back-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3); text-decoration: none; } -.empty-state p { - font-size: 1.1em; - margin-bottom: 30px; +/* ====== ๋ฐ˜์‘ํ˜• ====== */ +@media (max-width: 768px) { + .ranking-container { padding: 20px 15px; } + .ranking-header h1 { font-size: 2em; } + .ranking-grid { grid-template-columns: 1fr; gap: 20px; } + .ranking-card { padding: 18px; flex-direction: column; text-align: center; } + .product-image { width: 90px; height: 90px; } } -.back-btn { - background: linear-gradient(135deg, #667eea, #764ba2); - color: white; - border: none; - padding: 12px 25px; +.category-dropdown { + padding: 12px 20px; + border: 2px solid #667eea; + margin-bottom: 20px; border-radius: 25px; - cursor: pointer; - font-size: 1em; + background: white; + color: #333; + font-size: 14px; font-weight: 600; + cursor: pointer; + outline: none; transition: all 0.3s ease; - text-decoration: none; - display: inline-block; - margin-top: 20px; -} - -.back-btn:hover { - transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3); - text-decoration: none; -} - -@media (max-width: 768px) { - .ranking-container { - padding: 20px 15px; - } - - .ranking-header h1 { - font-size: 2em; - } - - .ranking-grid { - grid-template-columns: 1fr; - gap: 20px; - } - - .ranking-card { - padding: 20px; - flex-direction: column; - text-align: center; - } - - .product-image { - width: 100px; - height: 100px; - } + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.2); + min-width: 150px; } \ No newline at end of file diff --git a/staticfiles/css/products/user_ratings.css b/staticfiles/css/products/user_ratings.css new file mode 100644 index 0000000..b4e159f --- /dev/null +++ b/staticfiles/css/products/user_ratings.css @@ -0,0 +1,398 @@ +/* ์‚ฌ์šฉ์ž ๋ฆฌ๋ทฐ ํŽ˜์ด์ง€ ์Šคํƒ€์ผ */ +.user-ratings-container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +.page-header { + text-align: center; + margin-bottom: 40px; +} + +.page-header h1 { + font-size: 2.5rem; + color: #333; + margin-bottom: 10px; +} + +.page-header .subtitle { + color: #666; + font-size: 1.1rem; +} + +/* ์š”์•ฝ ์นด๋“œ */ +.ratings-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-bottom: 40px; +} + +.summary-card { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 30px 20px; + border-radius: 15px; + text-align: center; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; +} + +.summary-card:hover { + transform: translateY(-5px); +} + +.summary-number { + font-size: 2.5rem; + font-weight: bold; + margin-bottom: 10px; +} + +.summary-label { + font-size: 1rem; + opacity: 0.9; +} + +/* ํ•„ํ„ฐ ๋ฒ„ํŠผ */ +.ratings-filter { + display: flex; + justify-content: center; + gap: 10px; + margin-bottom: 30px; + flex-wrap: wrap; +} + +.filter-btn { + padding: 10px 20px; + border: 2px solid #ddd; + background: white; + color: #666; + border-radius: 25px; + cursor: pointer; + transition: all 0.3s ease; + font-weight: 500; +} + +.filter-btn:hover { + border-color: #667eea; + color: #667eea; +} + +.filter-btn.active { + background: #667eea; + border-color: #667eea; + color: white; +} + +/* ๋ฆฌ๋ทฐ ๋ชฉ๋ก */ +.ratings-list { + display: flex; + flex-direction: column; + gap: 20px; +} + +.rating-item { + background: white; + border: 1px solid #e9ecef; + border-radius: 12px; + padding: 25px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.rating-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +} + +.rating-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 15px; +} + +.product-info { + flex: 1; +} + +.product-name { + font-size: 1.3rem; + font-weight: bold; + color: #333; + margin: 0 0 5px 0; +} + +.product-brand { + color: #666; + font-size: 1rem; + margin: 0; +} + +.rating-info { + text-align: right; +} + +.stars { + display: flex; + gap: 2px; + margin-bottom: 8px; +} + +.stars .star { + font-size: 1.2rem; + color: #ddd; +} + +.stars .star.filled { + color: #ffd700; +} + +.rating-date { + color: #999; + font-size: 0.9rem; +} + +.rating-comment { + background: #f8f9fa; + padding: 15px; + border-radius: 8px; + margin-bottom: 15px; +} + +.rating-comment p { + margin: 0; + color: #555; + line-height: 1.6; +} + +.rating-actions { + display: flex; + gap: 10px; + justify-content: flex-end; +} + +.edit-btn, .view-product-btn { + padding: 8px 16px; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s ease; +} + +.edit-btn { + background: #667eea; + color: white; +} + +.edit-btn:hover { + background: #5a6fd8; +} + +.view-product-btn { + background: #f8f9fa; + color: #666; + border: 1px solid #ddd; +} + +.view-product-btn:hover { + background: #e9ecef; +} + +/* ๋นˆ ์ƒํƒœ */ +.no-ratings { + text-align: center; + padding: 60px 20px; + color: #666; +} + +.no-ratings-icon { + font-size: 4rem; + margin-bottom: 20px; +} + +.no-ratings h3 { + font-size: 1.5rem; + margin-bottom: 10px; + color: #333; +} + +.no-ratings p { + margin-bottom: 30px; + font-size: 1.1rem; +} + +.browse-products-btn { + display: inline-block; + padding: 12px 24px; + background: #667eea; + color: white; + text-decoration: none; + border-radius: 25px; + font-weight: 500; + transition: background 0.3s ease; +} + +.browse-products-btn:hover { + background: #5a6fd8; + color: white; +} + +/* ๋ชจ๋‹ฌ ์Šคํƒ€์ผ */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); +} + +.modal-content { + background-color: white; + margin: 5% auto; + padding: 0; + border-radius: 12px; + width: 90%; + max-width: 500px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); +} + +.modal-header { + padding: 20px 25px; + border-bottom: 1px solid #e9ecef; + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-header h3 { + margin: 0; + color: #333; +} + +.close { + color: #aaa; + font-size: 28px; + font-weight: bold; + cursor: pointer; + line-height: 1; +} + +.close:hover { + color: #333; +} + +.modal-body { + padding: 25px; +} + +.product-info-modal { + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid #f0f0f0; +} + +.product-info-modal h4 { + margin: 0 0 5px 0; + color: #333; + font-size: 1.2rem; +} + +.product-info-modal p { + margin: 0; + color: #666; +} + +.rating-input-modal { + margin-bottom: 20px; +} + +.rating-input-modal label { + display: block; + margin-bottom: 10px; + font-weight: 500; + color: #333; +} + +.stars-input-modal { + display: flex; + gap: 5px; +} + +.stars-input-modal .star { + font-size: 2rem; + color: #ddd; + cursor: pointer; + transition: color 0.2s ease; +} + +.stars-input-modal .star:hover, +.stars-input-modal .star.filled { + color: #ffd700; +} + +.comment-input-modal label { + display: block; + margin-bottom: 10px; + font-weight: 500; + color: #333; +} + +#modal-comment { + width: 100%; + min-height: 100px; + padding: 12px; + border: 1px solid #ddd; + border-radius: 6px; + resize: vertical; + font-family: inherit; + font-size: 0.9rem; +} + +#modal-comment:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.25); +} + +.modal-footer { + padding: 20px 25px; + border-top: 1px solid #e9ecef; + display: flex; + gap: 10px; + justify-content: flex-end; +} + +.save-btn, .delete-btn { + padding: 10px 20px; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: 500; + transition: all 0.2s ease; +} + +.save-btn { + background: #667eea; + color: white; +} + +.save-btn:hover { + background: #5a6fd8; +} + +.delete-btn { + background: #dc3545; + color: white; +} + +.delete-btn:hover { + background: #c82333; +} + + \ No newline at end of file diff --git a/staticfiles/css/recommendation/color_matrix_style.css b/staticfiles/css/recommendation/color_matrix_style.css index a351feb..f2e235c 100644 --- a/staticfiles/css/recommendation/color_matrix_style.css +++ b/staticfiles/css/recommendation/color_matrix_style.css @@ -657,7 +657,8 @@ main { } .toggle-checkbox { - display: none; /* ์‹ค์ œ ์ฒดํฌ๋ฐ•์Šค๋Š” ์ˆจ๊น€ */ + display: none; + /* ์‹ค์ œ ์ฒดํฌ๋ฐ•์Šค๋Š” ์ˆจ๊น€ */ } .toggle-label { @@ -675,8 +676,9 @@ main { } /* ์ฒดํฌ๋์„ ๋•Œ ์Šคํƒ€์ผ */ -.toggle-checkbox:checked + .toggle-label { - background-color: #8A2BE2; /* ๋ณด๋ผ์ƒ‰ ๊ณ„์—ด */ +.toggle-checkbox:checked+.toggle-label { + background-color: #8A2BE2; + /* ๋ณด๋ผ์ƒ‰ ๊ณ„์—ด */ } @media (max-width: 900px) { diff --git a/staticfiles/css/style.css b/staticfiles/css/style.css index d1ed04f..debf025 100644 --- a/staticfiles/css/style.css +++ b/staticfiles/css/style.css @@ -6,17 +6,24 @@ body { background-color: #f4f7f6; color: #333; line-height: 1.6; - cursor: url("{% static 'images/purple_cursor.svg' %}"), auto; + cursor: url("{% static 'images/purple_cursor.svg' %}"), + auto; } header { - background-color: #fff; + background-color: transparent; + background: linear-gradient(to bottom, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); padding: 20px 40px; - border-bottom: 1px solid #eee; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); transform: translateY(-100%); opacity: 0; - transition: transform 0.4s ease-out, opacity 0.4s ease-out; + transition: transform 0.4s ease-out, opacity 0.4s ease-out, background 0.5s ease; + position: sticky; + top: 0; + z-index: 999; +} + +header:hover { + background: linear-gradient(135deg, #fce4ec 0%, #f3e5f5 50%, #fff8e1 100%); } header.is-visible { @@ -26,56 +33,95 @@ header.is-visible { header nav { display: flex; - justify-content: space-between; + justify-content: center; align-items: center; - max-width: 1020px; + max-width: 1200px; margin: 0 auto; } -.nav-links { +.nav-links, +.nav-links-left { + transition: opacity 0.5s ease-in-out; display: flex; align-items: center; gap: 20px; + position: absolute; + align-self: flex-end; +} + +.nav-links { + right: 30px; +} + +.nav-links-left { + left: 30px; +} + +.nav-links a, +.nav-links-left a { + color: mediumpurple; +} + + +.nav-links-left a:after, +.nav-links a:after { + border: 1px solid rgba(255, 255, 255, 0); + bottom: 0; + content: " "; + display: block; + margin: 0 auto; + position: relative; + -webkit-transition: all .28s ease-in-out; + transition: all .28s ease-in-out; + width: 0; +} + +.nav-links-left a:hover:after, +.nav-links a:hover:after { + border-color: mediumpurple; + box-shadow: 3px 3px 3px gray; + transition: width 350ms ease-in-out; + width: 100%; } + .nav-link { text-decoration: none; color: #333; font-weight: 500; - padding: 8px 16px; - border-radius: 20px; + padding: 10px 18px; + border-radius: 25px; transition: all 0.3s ease; - font-size: 0.9em; } .nav-link:hover { - background: rgba(255, 107, 107, 0.1); + /* background: rgba(255, 107, 107, 0.1); color: #ff6b6b; - transform: translateY(-1px); + transform: translateY(-1px); */ } .liked-products-link { - background: linear-gradient(135deg, #ff6b6b, #ff8e8e); + /* background: linear-gradient(135deg, #ff6b6b, #ff8e8e); color: white; - box-shadow: 0 2px 8px rgba(255, 107, 107, 0.3); - + box-shadow: 0 2px 8px rgba(255, 107, 107, 0.3); */ } .liked-products-link:hover { - background: linear-gradient(135deg, #ff5252, #ff7676); + /* background: linear-gradient(135deg, #ff5252, #ff7676); color: white; + border-color: #ff5252; box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4); - text-decoration: none; + text-decoration: none; */ } header .logo { color: mediumpurple; - font-size: 1.8em; - font-weight: bold; + font-size: 2.2em; text-decoration: none; + font-weight: bold; } -header .login-icon:hover { +header a:hover { text-decoration: none; } @@ -304,16 +350,41 @@ input[type="email"] { /* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */ @media (max-width: 768px) { + header { + padding: 15px 20px; + } + + .logo { + font-size: 1.6em; + } + .nav-links { - gap: 10px; + gap: 15px; } - .nav-link { - padding: 6px 12px; + .nav-link, + .login-icon { + padding: 8px 14px; font-size: 0.8em; } +} + +@media (max-width: 480px) { + header { + padding: 10px 15px; + } + + .logo { + font-size: 1.4em; + } + + .nav-links { + gap: 10px; + } - .liked-products-link { - padding: 6px 12px; + .nav-link, + .login-icon { + padding: 6px 10px; + font-size: 0.75em; } } \ No newline at end of file diff --git a/staticfiles/css/upload/upload.css b/staticfiles/css/upload/upload.css index 40c7b23..cf3bd63 100644 --- a/staticfiles/css/upload/upload.css +++ b/staticfiles/css/upload/upload.css @@ -154,11 +154,101 @@ /* ๋™์  ์ถ”์ฒœ ์ œํ’ˆ ์˜์—ญ ์Šคํƒ€์ผ */ +.recommendations-section { + margin-top: 40px; +} + +.recommendations-section h3 { + text-align: center; + font-size: 1.8em; + color: #333; + margin-bottom: 30px; + background: linear-gradient(135deg, #667eea, #764ba2); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +/* ์ถ”์ฒœ ์Šฌ๋กฏ ์ปจํ…Œ์ด๋„ˆ */ +.recommendation-slots { + display: flex; + flex-direction: column; + gap: 15px; + margin-bottom: 30px; +} + +/* ๊ฐœ๋ณ„ ์ถ”์ฒœ ์Šฌ๋กฏ */ +.recommendation-slot { + background: white; + border-radius: 15px; + padding: 15px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); + border: 1px solid #f0f0f0; + transition: all 0.3s ease; +} + +.recommendation-slot:hover { + transform: translateY(-3px); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.12); +} + +/* ์Šฌ๋กฏ ํ—ค๋” */ +.slot-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 2px solid #f8f9fa; +} + +.slot-header h4 { + font-size: 1.2em; + font-weight: 600; + color: #333; + margin: 0; +} + +.product-count { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + padding: 4px 12px; + border-radius: 15px; + font-size: 0.8em; + font-weight: 600; +} + +/* ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋ฐ•์Šค */ +.category-box { + margin-top: 0; + padding: 0; + background: transparent; + display: flex; + flex-direction: row; + gap: 12px; + overflow-x: auto; + overflow-y: visible; + max-height: none; + padding-bottom: 8px; +} + +/* ๋นˆ ์ƒํƒœ ์Šคํƒ€์ผ */ +.empty-state { + text-align: center; + padding: 40px 20px; + color: #999; +} + +.empty-state p { + font-size: 0.9em; + margin: 0; + line-height: 1.4; +} + +/* ๊ธฐ์กด recommendation-box ์Šคํƒ€์ผ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) */ .recommendation-box { margin-top: 30px; padding: 20px; background: white; - display: flex; flex-wrap: nowrap; gap: 20px; @@ -170,6 +260,174 @@ display: none; } +/* ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ œํ’ˆ ์นด๋“œ */ +.category-box .product-card { + background: white; + border-radius: 10px; + padding: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border: 1px solid #eee; + transition: all 0.3s ease; + display: flex; + flex-direction: column; + height: auto; + min-height: 160px; + position: relative; + min-width: 220px; + max-width: 280px; + flex-shrink: 0; +} + +.category-box .product-card:hover { + transform: translateY(-3px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); + border-color: #667eea; +} + +.category-box .product-card img { + width: 100%; + height: 100px; + object-fit: cover; + border-radius: 6px; + margin-bottom: 10px; +} + +.category-box .product-card .product-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.category-box .product-card .brand { + font-size: 11px; + font-weight: 600; + color: #666; + margin-bottom: 2px; +} + +.category-box .product-card .name { + font-size: 12px; + font-weight: 500; + color: #333; + line-height: 1.3; + margin-bottom: 4px; + word-break: keep-all; + overflow-wrap: break-word; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.category-box .product-card .price { + font-size: 14px; + font-weight: 700; + color: #333; + margin-top: auto; +} + +.category-box .product-card .button-container { + display: flex; + gap: 6px; + margin-top: 8px; +} + +.category-box .product-card .recommendation-button { + flex: 1; + padding: 6px 10px; + border: none; + border-radius: 6px; + font-size: 10px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-decoration: none; + text-align: center; +} + +.category-box .product-card .view-detail-btn { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; +} + +.category-box .product-card .view-detail-btn:hover { + background: linear-gradient(135deg, #5a6fd8, #6a4190); + transform: translateY(-1px); +} + +.category-box .product-card .recommendation-button:not(.view-detail-btn) { + background: #f8f9fa; + color: #333; + border: 1px solid #e9ecef; +} + +.category-box .product-card .recommendation-button:not(.view-detail-btn):hover { + background: #e9ecef; + border-color: #667eea; +} + +/* ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์นด๋“œ์˜ ์ข‹์•„์š” ๋ฒ„ํŠผ ์Šคํƒ€์ผ */ +.category-box .product-card .like-button { + position: absolute; + top: 6px; + right: 6px; + background: rgba(255, 255, 255, 0.9); + border: 2px solid mediumpurple; + border-radius: 50%; + width: 24px; + height: 24px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + z-index: 10; + font-size: 10px; +} + +.category-box .product-card .like-button:hover { + background: mediumpurple; + color: white; + transform: scale(1.1); +} + +.category-box .product-card .like-button.liked { + background: mediumpurple; + color: white; + animation: heartBeat 0.6s ease-in-out; +} + +.category-box .product-card .like-count { + position: absolute; + top: 6px; + right: 34px; + background: rgba(255, 107, 107, 0.9); + color: white; + border-radius: 8px; + padding: 1px 5px; + font-size: 9px; + font-weight: 600; + min-width: 14px; + text-align: center; + z-index: 9; + display: none; + box-shadow: 0 2px 6px rgba(255, 107, 107, 0.3); + transition: all 0.3s ease; +} + +.category-box .product-card .like-count:hover { + transform: scale(1.05); +} + +/* ์ข‹์•„์š” ๋ฒ„ํŠผ ์• ๋‹ˆ๋ฉ”์ด์…˜ */ +@keyframes heartBeat { + 0% { transform: scale(1); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } +} + +/* ๊ธฐ์กด recommendation-box ์ œํ’ˆ ์นด๋“œ ์Šคํƒ€์ผ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) */ .recommendation-box .product-card { background: white; border-radius: 12px; @@ -671,6 +929,44 @@ .color-matrix-container { height: 400px; } + + /* ๋ชจ๋ฐ”์ผ์—์„œ ์ถ”์ฒœ ์Šฌ๋กฏ ์Šคํƒ€์ผ ์กฐ์ • */ + .recommendation-slots { + gap: 15px; + } + + .recommendation-slot { + padding: 15px; + } + + .category-box .product-card { + min-height: 180px; + min-width: 200px; + } + + .category-box .product-card img { + height: 100px; + } +} + +@media (max-width: 480px) { + .recommendation-slot { + padding: 12px; + } + + .slot-header h4 { + font-size: 1.1em; + } + + .category-box .product-card { + padding: 12px; + min-height: 160px; + min-width: 180px; + } + + .category-box .product-card img { + height: 90px; + } } @media (max-width: 1024px) and (min-width: 769px) { @@ -862,4 +1158,4 @@ .search-results-box li:hover { background-color: #f0f0f0; -} \ No newline at end of file +} diff --git a/staticfiles/js/main/main.js b/staticfiles/js/main/main.js index bbaceb3..8d8a2ad 100644 --- a/staticfiles/js/main/main.js +++ b/staticfiles/js/main/main.js @@ -202,5 +202,71 @@ document.addEventListener('DOMContentLoaded', function() { if (splashContainer) { splashContainer.classList.add('hidden'); } - }, 4000); + }, 2500); }); + + + // reveal + const _io = new IntersectionObserver((entries)=>entries.forEach(e=>{if(e.isIntersecting)e.target.classList.add('on');}),{threshold:.15}); + document.querySelectorAll('.pc-reveal').forEach(el=>_io.observe(el)); + + // mood palettes + const PC_MOOD = { + lovely: { + title:'๋Ÿฌ๋ธ”๋ฆฌ ๋ฌด๋“œ ยท ์ถ”์ฒœ ํŒ”๋ ˆํŠธ', + chips:[['#ff8a65','์ฝ”๋ž„'],['#f4a7b9','๋กœ์ฆˆํ•‘ํฌ'],['#e1f5fe','๋ฒ ์ด๋น„๋ธ”๋ฃจ'],['#fff4e6','ํฌ๋ฆผ์•„์ด๋ณด๋ฆฌ']], + tips:['ํ”ผ์น˜/ํ•‘ํฌํ†ค ๋ธ”๋Ÿฌ์…” ๋„“๊ฒŒ','๊ธ€๋กœ์‹œ ๋ฆฝ์œผ๋กœ ์ƒ๊ธฐ์žˆ๊ฒŒ','๋ฏธ๋‹ˆ๋ฉ€ํ•œ ์ฃผ์–ผ๋ฆฌ'] + }, + chic: { + title:'์‹œํฌ ๋ฌด๋“œ ยท ์ถ”์ฒœ ํŒ”๋ ˆํŠธ', + chips:[['#000000','๋ธ”๋ž™'],['#4b4b5b','๋‹คํฌ๋„ค์ด๋น„'],['#a0a3a7','์Šคํ†ค๊ทธ๋ ˆ์ด'],['#ffffff','ํ™”์ดํŠธ']], + tips:['๋ชจ๋…ธํ†ค ์˜์ƒ์œผ๋กœ ํ†ค์˜จํ†ค ์Šคํƒ€์ผ๋ง','๋ฆฝ์€ ๋”ฅํ•œ ๋ฒ„๊ฑด๋””/๋ ˆ๋“œ','์‹ค๋ฒ„ ๋˜๋Š” ํ”Œ๋ž˜ํ‹ฐ๋„˜ ์ฃผ์–ผ๋ฆฌ'] + }, + natural: { + title:'๋‚ด์ถ”๋Ÿด ๋ฌด๋“œ ยท ์ถ”์ฒœ ํŒ”๋ ˆํŠธ', + chips:[['#c8e6c9','๋ฏผํŠธ'],['#a67c52','์นด๋ฉœ'],['#d2b48c','๋ฒ ์ด์ง€'],['#c8d5b9','์„ธ์ด์ง€๊ทธ๋ฆฐ']], + tips:['์ž์—ฐ์Šค๋Ÿฌ์šด ์ฑ„๋„์˜ ์˜ท์œผ๋กœ ํ†ค ๋งž์ถ”๊ธฐ','๋ˆ„๋“œ/์ฝ”๋ž„ ๊ณ„์—ด ๋ฆฝ','๊ณจ๋“œ ๋˜๋Š” ์›์„ ์•ก์„ธ์„œ๋ฆฌ'] + }, + casual: { + title:'์บ์ฃผ์–ผ ๋ฌด๋“œ ยท ์ถ”์ฒœ ํŒ”๋ ˆํŠธ', + chips:[['#ffb300','๋จธ์Šคํƒ€๋“œ์˜๋กœ'],['#3d5afe','๋กœ์–„๋ธ”๋ฃจ'],['#e53935','๋ ˆ๋“œ'],['#fff4e6','์•„์ด๋ณด๋ฆฌ']], + tips:['๋น„๋น„๋“œ ์ปฌ๋Ÿฌ๋กœ ํ•œ๋‘ ๊ณณ ํฌ์ธํŠธ','์Šค๋‹ˆ์ปค์ฆˆ/์บก๋ชจ์ž๋กœ ํ™œ๋™์„ฑ ๊ฐ•์กฐ','๊ท€์—ฝ๊ณ  ์ž‘์€ ์•ก์„ธ์„œ๋ฆฌ'] + }, + elegant: { + title:'๊ณ ๊ธ‰์Šค๋Ÿฌ์šด ๋ฌด๋“œ ยท ์ถ”์ฒœ ํŒ”๋ ˆํŠธ', + chips:[['#9e3c3e','์™€์ธ'],['#3b3b3c','์ฐจ์ฝœ'],['#bfa57f','๋ธŒ๋ก ์ฆˆ'],['#d4c4b6','๋ฒ ์ด์ง€']], + tips:['์‹คํฌ/์ƒˆํ‹ด ๋“ฑ ๊ณ ๊ธ‰ ์†Œ์žฌ ํ™œ์šฉ','์ž…์ˆ ์€ ๋”ฅ๋ ˆ๋“œ/๋ง๋ฆฐ์žฅ๋ฏธ','์ง„์ฃผ ๋˜๋Š” ๊ณจ๋“œ ์ฃผ์–ผ๋ฆฌ'] + }, + modern: { + title:'๋ชจ๋˜ ๋ฌด๋“œ ยท ์ถ”์ฒœ ํŒ”๋ ˆํŠธ', + chips:[['#78909C','์Šค๋ชจํ‚ค๋ธ”๋ฃจ'],['#4b4b5b','๋‹คํฌ๊ทธ๋ ˆ์ด'],['#9aa0a6','์ฟจ๊ทธ๋ ˆ์ด'],['#ffffff','ํ™”์ดํŠธ']], + tips:['์ ˆ์ œ๋œ ์‹ค๋ฃจ์—ฃ์˜ ์˜์ƒ','๋ฆฝ์€ ๋กœ์ฆˆ ๋˜๋Š” ๋ชจ๋ธŒ MLBB','๋ฏธ๋‹ˆ๋ฉ€ํ•œ ์‹ค๋ฒ„ ์ฃผ์–ผ๋ฆฌ'] + }, + purity: { + title:'์ฒญ์ˆœ ๋ฌด๋“œ ยท ์ถ”์ฒœ ํŒ”๋ ˆํŠธ', + chips:[['#e1f5fe','์Šค์นด์ด๋ธ”๋ฃจ'],['#f8bbd0','๋ผ์ดํŠธํ•‘ํฌ'],['#b19cd9','๋ผ๋ฒค๋”'],['#fff8e1','์•„์ด๋ณด๋ฆฌ']], + tips:['ํˆฌ๋ช…ํ•œ ํ”ผ๋ถ€ ํ‘œํ˜„์— ์ง‘์ค‘','์ˆ˜์ฑ„ํ™”์ฒ˜๋Ÿผ ์—ฐํ•œ ํ•‘ํฌ ๋ธ”๋Ÿฌ์…”','๊ธ€๋กœ์‹œํ•œ ๋ฆฝ ์—ฐ์ถœ'] + }, + hip: { + title:'ํž™ ๋ฌด๋“œ ยท ์ถ”์ฒœ ํŒ”๋ ˆํŠธ', + chips:[['#b39ddb','๋ฐ”์ด์˜ฌ๋ ›'],['#c0c0c0','์‹ค๋ฒ„'],['#6c757d','๊ทธ๋ ˆ์ด'],['#f44336','๊ฐ•๋ ฌํ•œ๋ ˆ๋“œ']], + tips:['์˜ค๋ฒ„์‚ฌ์ด์ฆˆ ํ• ์˜์ƒ ํ™œ์šฉ','์œ ๋‹ˆํฌํ•œ ํŒจํ„ด์ด๋‚˜ ํ”„๋ฆฐํŒ…','๊ณผ๊ฐํ•œ ์•ก์„ธ์„œ๋ฆฌ ๋งค์น˜'] + } +}; + + const moodBoard = document.getElementById('pcMoodBoard'); + if (moodBoard){ + const chipsBox = document.getElementById('pcMoodChips'); + const tipsBox = document.getElementById('pcMoodTips'); + const titleEl = document.getElementById('pcMoodTitle'); + function renderMood(key){ + const d = PC_MOOD[key]; if(!d) return; + titleEl.textContent = d.title; + chipsBox.innerHTML = d.chips.map(([c,l])=>``).join(''); + tipsBox.innerHTML = d.tips.map(t=>`
  • ${t}
  • `).join(''); + } + renderMood('excited'); + moodBoard.querySelectorAll('[data-mood]').forEach(btn=>{ + btn.addEventListener('click', ()=> renderMood(btn.dataset.mood)); + }); + } diff --git a/staticfiles/js/products/ranking_filter.js b/staticfiles/js/products/ranking_filter.js new file mode 100644 index 0000000..c5208fb --- /dev/null +++ b/staticfiles/js/products/ranking_filter.js @@ -0,0 +1,42 @@ +document.addEventListener('DOMContentLoaded', function() { + const filterForm = document.getElementById('filterForm'); + const rankingGrid = document.getElementById('rankingGrid'); + + filterForm.addEventListener('submit', function(e) { + e.preventDefault(); // ๊ธฐ๋ณธ ํผ ์ œ์ถœ ๋ง‰๊ธฐ + + const formData = new FormData(filterForm); + const data = Object.fromEntries(formData.entries()); + + fetch('/products/filter_ranking/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') // Django CSRF ์ฒ˜๋ฆฌ + }, + body: JSON.stringify(data) + }) + .then(response => response.text()) + .then(html => { + rankingGrid.innerHTML = html; // _ranking_grid.html ๋‚ด์šฉ์œผ๋กœ ๊ต์ฒด + }) + .catch(err => console.error('ํ•„ํ„ฐ Ajax ์˜ค๋ฅ˜:', err)); + }); + }); + + // CSRF token ๊ฐ€์ ธ์˜ค๊ธฐ ํ•จ์ˆ˜ + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let cookie of cookies) { + cookie = cookie.trim(); + if (cookie.startsWith(name + '=')) { + cookieValue = decodeURIComponent(cookie.slice(name.length + 1)); + break; + } + } + } + return cookieValue; + } + \ No newline at end of file diff --git a/staticfiles/js/recommendation/color_analyzer.js b/staticfiles/js/recommendation/color_analyzer.js index 5c00280..2990434 100644 --- a/staticfiles/js/recommendation/color_analyzer.js +++ b/staticfiles/js/recommendation/color_analyzer.js @@ -216,8 +216,53 @@ function getCookie(name) { } function renderRecommendations(products) { - const box = document.getElementById("recommendation-box"); + // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„๋กœ ์ œํ’ˆ ๋ถ„๋ฅ˜ + const categorizedProducts = { + 'Lips': [], + 'blush': [], + 'eyeshadow': [] + }; + + // ์ œํ’ˆ์„ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„๋กœ ๋ถ„๋ฅ˜ + products.forEach(product => { + const category = product.category; + if (category && categorizedProducts.hasOwnProperty(category)) { + categorizedProducts[category].push(product); + } else { + // ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋งค์นญ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ฒ˜๋ฆฌ + categorizedProducts['Lips'].push(product); + } + }); + + // ๊ฐ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„๋กœ ์ œํ’ˆ ๋ Œ๋”๋ง + renderCategoryProducts('lip', categorizedProducts['Lips']); + renderCategoryProducts('blush', categorizedProducts['blush']); + renderCategoryProducts('eyeshadow', categorizedProducts['eyeshadow']); + + // ์ œํ’ˆ ๊ฐœ์ˆ˜ ์—…๋ฐ์ดํŠธ + updateProductCounts(categorizedProducts); +} + +function renderCategoryProducts(categoryType, products) { + const boxId = `${categoryType}-recommendation-box`; + const box = document.getElementById(boxId); + + if (!box) return; + + // ๊ธฐ์กด ๋‚ด์šฉ ์ œ๊ฑฐ box.innerHTML = ""; + + if (products.length === 0) { + // ์ œํ’ˆ์ด ์—†๋Š” ๊ฒฝ์šฐ ๋นˆ ์ƒํƒœ ํ‘œ์‹œ + box.innerHTML = ` +
    +

    ํ•ด๋‹น ์นดํ…Œ๊ณ ๋ฆฌ์˜ ์ถ”์ฒœ ์ œํ’ˆ์ด ์—†์–ด์š”

    +
    + `; + return; + } + + // ์ œํ’ˆ ์นด๋“œ ์ƒ์„ฑ products.forEach((p, index) => { const card = document.createElement("div"); card.classList.add("product-card"); @@ -231,9 +276,7 @@ function renderRecommendations(products) { const uniqueId = `${brand}-${name}-${price}-${imgHash}`.substring(0, 60); card.dataset.productId = p.id || uniqueId; - // ๋””๋ฒ„๊น…: ์‹ค์ œ ์ œํ’ˆ ID์™€ ์ƒ์„ฑ๋œ ID ์ถœ๋ ฅ console.log(`์ œํ’ˆ ${index}: ์›๋ณธ ID=${p.id}, ์ƒ์„ฑ๋œ ID=${card.dataset.productId}`); - console.log(`Color analyzer card ${index}: ID=${card.dataset.productId}, Name=${p.color_name || p.name}`); card.innerHTML = ` @@ -242,7 +285,7 @@ function renderRecommendations(products) { ${p.name}
    ${p.brand} ${p.category}
    -
    ${p.color_name}
    +
    ${p.color_name || p.name}
    ${p.price}
    @@ -250,6 +293,7 @@ function renderRecommendations(products) { ๊ตฌ๋งคํ•˜๊ธฐ
    `; + box.appendChild(card); // ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ์นด๋“œ์˜ ์ข‹์•„์š” ์ƒํƒœ ๋ณต์› @@ -265,11 +309,24 @@ function renderRecommendations(products) { }); } - // ๋””๋ฒ„๊น…: ์นด๋“œ ์ •๋ณด ์ถœ๋ ฅ console.log(`์ถ”์ฒœ ์ œํ’ˆ ์นด๋“œ ์ƒ์„ฑ: ID=${p.id}, ์ด๋ฆ„=${p.name}, ๋ธŒ๋žœ๋“œ=${p.brand}`); }); } +function updateProductCounts(categorizedProducts) { + // ๋ฆฝ ์ œํ’ˆ ๊ฐœ์ˆ˜ ์—…๋ฐ์ดํŠธ + const lipCount = document.querySelector('#lip-recommendation-box').closest('.recommendation-slot').querySelector('.product-count'); + if (lipCount) lipCount.textContent = `${categorizedProducts['Lips'].length}๊ฐœ`; + + // ๋ธ”๋Ÿฌ์…” ์ œํ’ˆ ๊ฐœ์ˆ˜ ์—…๋ฐ์ดํŠธ + const blushCount = document.querySelector('#blush-recommendation-box').closest('.recommendation-slot').querySelector('.product-count'); + if (blushCount) blushCount.textContent = `${categorizedProducts['blush'].length}๊ฐœ`; + + // ์•„์ด์„€๋„์šฐ ์ œํ’ˆ ๊ฐœ์ˆ˜ ์—…๋ฐ์ดํŠธ + const eyeshadowCount = document.querySelector('#eyeshadow-recommendation-box').closest('.recommendation-slot').querySelector('.product-count'); + if (eyeshadowCount) eyeshadowCount.textContent = `${categorizedProducts['eyeshadow'].length}๊ฐœ`; +} + function displayRecommendationsOnMatrix(products) { const productsContainer = document.querySelector('.color-matrix-container'); if (!productsContainer) return; From a78d8b9b5b4fb0ad0686d69461e1efb2292b52f0 Mon Sep 17 00:00:00 2001 From: ZinYan Date: Sun, 17 Aug 2025 23:35:52 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[deploy]=20css=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/main/main.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/main/main.html b/templates/main/main.html index a0aad0c..8899f10 100644 --- a/templates/main/main.html +++ b/templates/main/main.html @@ -337,3 +337,4 @@

    ๐Ÿ† ํ˜„์žฌ ์ธ๊ธฐ ์•„์ดํ…œ

    {% endblock %} + From d9331b7622f438c3cb69f739290c3cc0abc594ed Mon Sep 17 00:00:00 2001 From: ZinYan Date: Mon, 18 Aug 2025 12:15:58 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[feat]=20=EC=B6=94=EC=B2=9C=20=EC=A0=9C?= =?UTF-8?q?=ED=92=88=2024=EC=8B=9C=EA=B0=84=EB=A7=88=EB=8B=A4=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- moodico/products/utils/scraper.py | 77 +++++++++++++++++++++++++++++++ moodico/products/views.py | 35 -------------- moodico/recommendation/views.py | 73 ++++++++++++++++------------- templates/main/main.html | 6 +-- 4 files changed, 118 insertions(+), 73 deletions(-) create mode 100644 moodico/products/utils/scraper.py diff --git a/moodico/products/utils/scraper.py b/moodico/products/utils/scraper.py new file mode 100644 index 0000000..f4fe35b --- /dev/null +++ b/moodico/products/utils/scraper.py @@ -0,0 +1,77 @@ +# utils/scraper.py +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from webdriver_manager.chrome import ChromeDriverManager + +def scrape_oliveyoung_products(max_items=10): + # Chrome config + chrome_options = webdriver.ChromeOptions() + chrome_options.add_argument("--headless") + chrome_options.add_argument("--no-sandbox") + chrome_options.add_argument("--disable-dev-shm-usage") + chrome_options.add_argument("--disable-gpu") + chrome_options.add_argument( + "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" + ) + + # Browser ์‹œ์ž‘ + driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options) + products = [] + + try: + # Target + url = ( + "https://www.oliveyoung.co.kr/store/main/getBestList.do" + "?dispCatNo=900000100100001&fltDispCatNo=10000010002&pageIdx=1&rowsPerPage=10" + ) + driver.get(url) + + wait = WebDriverWait(driver, 5) # 5์ดˆ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ (์•„์ดํ…œ๋“ค ์žˆ๋Š”์ง€ ์ฒดํฌํ•˜๋Š” ๋™์•ˆ) + wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "ul.cate_prd_list"))) + items = driver.find_elements(By.CSS_SELECTOR, "ul.cate_prd_list li") #all
  • + + # Hard cap at max_items + for item in items[:max_items]: + try: + # ์ œํ’ˆ ์ •๋ณด + prd_info = item.find_element(By.CSS_SELECTOR, "div.prd_info") + link_tag = prd_info.find_element(By.CSS_SELECTOR, "a.prd_thumb") + product_url = link_tag.get_attribute("href") + # ์ œํ’ˆ ์ด๋ฏธ์ง€ + img_tag = link_tag.find_element(By.TAG_NAME, "img") + image_src = img_tag.get_attribute("src") or img_tag.get_attribute("data-original") or "" + image_alt = img_tag.get_attribute("alt") or "" + + brand_name = prd_info.find_element(By.CSS_SELECTOR, "span.tx_brand").text.strip() + product_name = prd_info.find_element(By.CSS_SELECTOR, "p.tx_name").text.strip() + price_original = prd_info.find_element(By.CSS_SELECTOR, "p.prd_price span.tx_org").text.strip() + + # ์ œํ’ˆ ํƒœํฌ๋“ค + flags = [] + try: + flag_spans = prd_info.find_elements(By.CSS_SELECTOR, "p.prd_flag span.icon_flag") + flags = [flag.text.strip() for flag in flag_spans if flag.text.strip()] + except Exception: + pass + + products.append({ + "product_url": product_url, + "brand_name": brand_name, + "product_name": product_name, + "image_src": image_src, + "image_alt": image_alt, + "price_original": price_original, + "flags": flags, + }) + + if len(products) >= max_items: + break + except Exception: + continue + finally: + driver.quit() + + return products diff --git a/moodico/products/views.py b/moodico/products/views.py index 876e914..f6fb6c3 100644 --- a/moodico/products/views.py +++ b/moodico/products/views.py @@ -55,7 +55,6 @@ def product_detail(request, product_id): return render(request, 'products/detail.html', {'product': product}) def crawled_product_detail(request, crawled_id): - # crawled_id -> a4c0a977-cced-4ce8-abea-f718dcff8325 """ํฌ๋กค๋ง๋œ ์ œํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€ ๋ทฐ""" try: logger.info(f"ํฌ๋กค๋ง๋œ ์ œํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€ ์š”์ฒญ: crawled_id = {crawled_id}") @@ -71,7 +70,6 @@ def crawled_product_detail(request, crawled_id): for p in products: if p.get('id') == crawled_id: - # p.get('id') -> a4c0a977-cced-4ce8-abea-f718dcff8325 product = p break print('...',crawled_id) @@ -92,39 +90,6 @@ def crawled_product_detail(request, crawled_id): average_rating = all_reviews.aggregate(avg=Avg('rating')).get('avg') or 0 average_rating = round(average_rating, 2) - # if not product: - # return render(request, 'products/detail.html', { - # 'error': '์ œํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.', - # 'product': None - # }) - - # # ํ•ด๋‹น ์ œํ’ˆ์˜ ๋ฆฌ๋ทฐ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ - # from moodico.users.utils import get_user_from_request - # user = get_user_from_request(request) - - # # ์ œํ’ˆ ID๋กœ ๋ฆฌ๋ทฐ ์ฐพ๊ธฐ (crawled_id ์‚ฌ์šฉ) - # user_review = None - # print('..',ProductRating.objects.all()) - # if user: - # try: - # user_review = ProductRating.objects.get( - # user=user, - # product_id=crawled_id - # ) - # except ProductRating.DoesNotExist: - # pass - - # # ์ œํ’ˆ์˜ ๋ชจ๋“  ๋ฆฌ๋ทฐ ๊ฐ€์ ธ์˜ค๊ธฐ - # # []> - # all_reviews = ProductRating.objects.filter(product_id=crawled_id).order_by('-created_at') - - # # ํ‰๊ท  ๋ณ„์ ๊ณผ ํ‰๊ฐ€ ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ - # total_ratings = all_reviews.count() - # if total_ratings > 0: - # total_score = sum(review.rating for review in all_reviews) - # average_rating = round(total_score / total_ratings, 1) - # else: - # average_rating = 0.0 context = { 'product': product, 'user_review': user_review, diff --git a/moodico/recommendation/views.py b/moodico/recommendation/views.py index 2b4d0f2..7fadc1d 100644 --- a/moodico/recommendation/views.py +++ b/moodico/recommendation/views.py @@ -8,53 +8,60 @@ from sklearn.metrics.pairwise import cosine_similarity # Create your views here. -# def my_item_recommendation(request): -# # Get recommended or default products -# search_results = get_top_liked_products(limit=10) -# recommended_items = [] # Set this if you want a separate recommended section -# print("....",search_results) - -# return render( -# request, -# 'upload/upload.html', -# { -# 'search_results': search_results, -# 'recommended_items': recommended_items -# } -# ) - -def get_recommendation_list(): - # JSON ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์‹ฑ (์‹ค์ œ๋กœ๋Š” DB๋‚˜ API์—์„œ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ์Œ) - products_path = 'static/data/advertise_products.json' - with open(products_path, 'r', encoding='utf-8') as f: - raw_data = json.load(f) - - # ํƒœ๊ทธ ์ถ”์ถœ ๊ทœ์น™ ์˜ˆ์‹œ (์ฒซ๋ฒˆ์งธ flag ์‚ฌ์šฉ or None) +from moodico.products.utils.scraper import scrape_oliveyoung_products +import time +from django.core.cache import cache + +CACHE_KEY = "oliveyoung_bestlist_v1" +CACHE_TTL = 60 * 60 * 24 # 24 hours + +def make_search_results(raw_data): def get_tag(flags): for tag in ['๊ธ€๋กœ์‹œ', 'matte', 'glossy', '์ฆ์ •', '์„ธ์ผ', '์ฟ ํฐ', '์˜ค๋Š˜๋“œ๋ฆผ']: if tag in flags: return tag return flags[0] if flags else '-' - search_results = [ + return [ { - "brand": item["brand_name"], - "name": item["product_name"], - "image": item["image_src"], - "price": item["price_original"].replace("~", ""), + "brand": item.get("brand_name", ""), + "name": item.get("product_name", ""), + "image": item.get("image_src", ""), + "price": (item.get("price_original", "") or "").replace("~", ""), "tag": get_tag(item.get("flags", [])), - "url": item["product_url"], + "url": item.get("product_url", ""), } for item in raw_data ] - return search_results + +def get_recommendation_list(force_refresh=False): + cached = cache.get(CACHE_KEY) + # ์บ์‹œ๊ฐ€ ์—†๊ฑฐ๋‚˜ force_refresh(์ž๋ฐœ์ ์ธ refresh)์ด๋ฉด + if (not cached) or force_refresh: + raw_data = scrape_oliveyoung_products() + search_results = make_search_results(raw_data) + payload = { + "results": search_results, + "fetched_at": int(time.time()), # ์–ธ์ œ refresh ๋๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ๊ฒŒ + } + cache.set(CACHE_KEY, payload, CACHE_TTL) + return payload + + return cached def my_item_recommendation(request): - search_results = get_recommendation_list() - return render(request, 'upload/upload.html', { - "search_results": search_results - }) + # ์ž๋ฐœ์ ์œผ๋กœ ํ™•์ธ ํ•˜๊ณ  ์‹ถ์„๋•Œ: /upload/?refresh=1 + force = request.GET.get("refresh") == "1" + data = get_recommendation_list(force_refresh=force) + return render( + request, + "upload/upload.html", + { + "search_results": data["results"], + "fetched_at": data["fetched_at"], + }, + ) @csrf_exempt def recommend_by_color(request): diff --git a/templates/main/main.html b/templates/main/main.html index 8899f10..9de887a 100644 --- a/templates/main/main.html +++ b/templates/main/main.html @@ -298,11 +298,7 @@

    ๐Ÿ† ํ˜„์žฌ ์ธ๊ธฐ ์•„์ดํ…œ

    - {% comment %} [{'product_id': 'ec733645-2d66-4fb4-b207-d740680acd73', - 'product_name': '04 ๋นˆํ‹ฐ์ง€ ์˜ค์…˜', 'product_brand': 'romand', - 'product_price': '9,900์›', 'product_image': - 'https://romand.io/images/product/343/BZgkQP0CTQ1Wb8FTVcLlVBqfPwSBpyZ3BeEQVdCu.jpg', - 'like_count': 1}, .. ] {% endcomment %} {% for product in top_liked_products %} + {% for product in top_liked_products %}
    {{ forloop.counter }}
    Date: Mon, 18 Aug 2025 13:20:15 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[bug]=20=EC=B6=94=EC=B2=9C=20=EC=A0=9C?= =?UTF-8?q?=ED=92=88=20=EB=A1=9C=EC=A7=81=20=EB=B0=B0=ED=8F=AC=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20chrome=EB=A1=9C=EB=94=A9=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- moodico/products/utils/scraper.py | 33 ++++++++++++++++++++----------- moodico/recommendation/views.py | 2 +- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/moodico/products/utils/scraper.py b/moodico/products/utils/scraper.py index f4fe35b..b086ada 100644 --- a/moodico/products/utils/scraper.py +++ b/moodico/products/utils/scraper.py @@ -5,20 +5,32 @@ from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager +import sys -def scrape_oliveyoung_products(max_items=10): +def _build_chrome_driver(): # Chrome config - chrome_options = webdriver.ChromeOptions() - chrome_options.add_argument("--headless") - chrome_options.add_argument("--no-sandbox") - chrome_options.add_argument("--disable-dev-shm-usage") - chrome_options.add_argument("--disable-gpu") - chrome_options.add_argument( - "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" + options = webdriver.ChromeOptions() + options.add_argument("--headless=new") + options.add_argument("--no-sandbox") + options.add_argument("--disable-dev-shm-usage") + options.add_argument("--disable-gpu") + options.add_argument( + "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" ) - # Browser ์‹œ์ž‘ - driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options) + # Linux (Ubuntu) ๋ฐฐํฌ ์„œ๋ฒ„ + if sys.platform.startswith("linux"): + options.binary_location = "/usr/bin/chromium-browser" + service = Service("/usr/bin/chromedriver") + + return webdriver.Chrome(service=service, options=options) + + # macOS/Windows or fallback -> use webdriver_manager + return webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options) + +def scrape_oliveyoung_products(max_items=10): + driver = _build_chrome_driver() products = [] try: @@ -33,7 +45,6 @@ def scrape_oliveyoung_products(max_items=10): wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "ul.cate_prd_list"))) items = driver.find_elements(By.CSS_SELECTOR, "ul.cate_prd_list li") #all
  • - # Hard cap at max_items for item in items[:max_items]: try: # ์ œํ’ˆ ์ •๋ณด diff --git a/moodico/recommendation/views.py b/moodico/recommendation/views.py index 7fadc1d..7ba6581 100644 --- a/moodico/recommendation/views.py +++ b/moodico/recommendation/views.py @@ -50,7 +50,7 @@ def get_recommendation_list(force_refresh=False): return cached def my_item_recommendation(request): - # ์ž๋ฐœ์ ์œผ๋กœ ํ™•์ธ ํ•˜๊ณ  ์‹ถ์„๋•Œ: /upload/?refresh=1 + # ์ž๋ฐœ์ ์œผ๋กœ ํ™•์ธ ํ•˜๊ณ  ์‹ถ์„๋•Œ: /?refresh=1 force = request.GET.get("refresh") == "1" data = get_recommendation_list(force_refresh=force)