diff --git a/moodico/products/utils/scraper.py b/moodico/products/utils/scraper.py new file mode 100644 index 0000000..b086ada --- /dev/null +++ b/moodico/products/utils/scraper.py @@ -0,0 +1,88 @@ +# 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 +import sys + +def _build_chrome_driver(): + # Chrome config + 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" + ) + + # 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: + # 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
  • + + 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..7ba6581 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 - }) + # 자발적으로 확인 하고 싶을때: /?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/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; diff --git a/templates/main/main.html b/templates/main/main.html index aa35dbc..9de887a 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 %}

    @@ -296,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 }}
    🏆 현재 인기 아이템 {% endblock %} +