developerDH commited on
Commit
9902efa
·
verified ·
1 Parent(s): 38302a5

Upload 8 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ final_prod_data.csv filter=lfs diff=lfs merge=lfs -text
37
+ final_review_df.csv filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,982 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """app.ipynb
3
+
4
+ Automatically generated by Colab.
5
+
6
+ Original file is located at
7
+ https://colab.research.google.com/drive/1hl-apQjNm2ijxBoDj-PV0ua7-4taVJo2
8
+ """
9
+
10
+ import os
11
+ import sys
12
+
13
+ __import__('pysqlite3')
14
+ import sys
15
+ sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')
16
+
17
+ import shutil
18
+ import torch
19
+ import re
20
+ import chromadb
21
+ from langchain_chroma import Chroma
22
+ import pandas as pd
23
+ import gradio as gr
24
+ import ast
25
+ import numpy as np
26
+ from PIL import Image
27
+ from collections import Counter, defaultdict
28
+ from transformers import CLIPProcessor, CLIPModel
29
+ from langchain_huggingface import HuggingFaceEmbeddings
30
+ from langchain_openai import ChatOpenAI
31
+ from langchain_core.prompts import PromptTemplate
32
+ from sentence_transformers import util
33
+
34
+ import sys
35
+ import gspread
36
+ from oauth2client.service_account import ServiceAccountCredentials
37
+ import datetime
38
+ import json
39
+
40
+ # [1] 구글 시트 연결 설정 함수
41
+ def get_google_sheet():
42
+ # 1. 인증 범위 설정
43
+ scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive']
44
+ # 2. 다운받은 키 파일 이름 (같은 폴더에 있어야 함)
45
+ creds = ServiceAccountCredentials.from_json_keyfile_name('fashion_key.json', scope)
46
+ client = gspread.authorize(creds)
47
+
48
+ # 3. 스프레드시트 열기 (공유한 파일 이름 정확히 입력!)
49
+ sh = client.open('패션추천시스템_사용자정보')
50
+ return sh
51
+
52
+ # [2] 검색 데이터 저장 함수 (시트1에 저장)
53
+ def log_search_to_sheet(bg, cat, min_p, max_p, style, txt, check, persona):
54
+ try:
55
+ sh = get_google_sheet()
56
+ worksheet = sh.get_worksheet(0) # 첫 번째 시트 선택
57
+
58
+ # 헤더가 없으면 추가 (옵션)
59
+ if not worksheet.row_values(1):
60
+ worksheet.append_row(['시간', '브랜드', '카테고리', '최소가격', '최대가격', '스타일', '텍스트', '성별교차', '결과페르소나'])
61
+
62
+ # 데이터 추가
63
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
64
+ worksheet.append_row([timestamp, bg, cat, min_p, max_p, style, txt, "O" if check else "X", persona])
65
+ except Exception as e:
66
+ print(f"구글 시트 저장 실패: {e}")
67
+
68
+ # [3] 사이즈 추천 데이터 저장 함수 (시트2에 저장)
69
+ def log_size_to_sheet(sel_product, u_info, rec_size, reason):
70
+ try:
71
+ sh = get_google_sheet()
72
+ # 두 번째 시트가 없으면 생성, 있으면 가져오기
73
+ try:
74
+ worksheet = sh.get_worksheet(1)
75
+ except:
76
+ worksheet = sh.add_worksheet(title="사이즈추천", rows="100", cols="20")
77
+
78
+ if not worksheet.row_values(1):
79
+ worksheet.append_row(['시간', '선택상품', '유저정보', '추천사이즈', '추천이유'])
80
+
81
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
82
+ user_info_str = json.dumps(u_info, ensure_ascii=False)
83
+ worksheet.append_row([timestamp, sel_product, user_info_str, rec_size, reason])
84
+ except Exception as e:
85
+ print(f"구글 시트 저장 실패: {e}")
86
+
87
+ os.environ["OPENAI_API_KEY"] = os.environ.get("OPENAI_API_KEY")
88
+ device = "cuda" if torch.cuda.is_available() else "cpu"
89
+
90
+ base_path = "."
91
+
92
+ product_db_path = os.path.join(base_path, "product_db")
93
+ review_db_path = os.path.join(base_path, "review_db")
94
+ style_db_path = os.path.join(base_path, "style_db")
95
+
96
+ product_csv_path = os.path.join(base_path, "final_prod_data.csv")
97
+ size_csv_path = os.path.join(base_path, "size_sorted.csv")
98
+ review_csv_path = os.path.join(base_path, "final_review_df.csv")
99
+
100
+
101
+ try:
102
+ prod_data = pd.read_csv(product_csv_path)
103
+ prod_data['primary_id'] = prod_data['primary_id'].astype(str)
104
+ except:
105
+ prod_data = pd.DataFrame()
106
+
107
+ try:
108
+ prod_size_df = pd.read_csv(size_csv_path)
109
+ prod_size_df = prod_size_df.replace('-', np.nan)
110
+ prod_size_df['primary_id'] = prod_size_df['primary_id'].astype(str)
111
+ except:
112
+ prod_size_df = pd.DataFrame()
113
+
114
+ try:
115
+ review_df = pd.read_csv(review_csv_path)
116
+ review_df['primary_id'] = review_df['primary_id'].astype(int)
117
+ review_ids = set(review_df['primary_id'].astype(str))
118
+ except:
119
+ print(f"리뷰 데이터 로드 실패: {e}")
120
+ review_ids = set()
121
+
122
+ clip_model = CLIPModel.from_pretrained("patrickjohncyh/fashion-clip").to(device)
123
+ clip_processor = CLIPProcessor.from_pretrained("patrickjohncyh/fashion-clip")
124
+
125
+ review_emb_func = HuggingFaceEmbeddings(
126
+ model_name='jhgan/ko-sroberta-multitask',
127
+ model_kwargs={'device': device},
128
+ encode_kwargs={'normalize_embeddings': True}
129
+ )
130
+
131
+ llm = ChatOpenAI(model = "gpt-4o-mini", temperature=0.7) # 결과값 확인 후 temperature 값 조정 필요
132
+
133
+ try:
134
+ product_client = chromadb.PersistentClient(path=product_db_path)
135
+ product_collection = product_client.get_collection("product_vectorstore")
136
+ except Exception as e:
137
+ print(f"{e} DB 연결 실패")
138
+
139
+ try:
140
+ style_client = chromadb.PersistentClient(path=style_db_path)
141
+ style_collection = style_client.get_collection("style_vectorstore")
142
+ except Exception as e:
143
+ print(f"{e} DB 연결 실패")
144
+
145
+ try:
146
+ review_db = Chroma(
147
+ persist_directory=review_db_path,
148
+ embedding_function=review_emb_func,
149
+ collection_name="review_vectorstore"
150
+ )
151
+ except Exception as e:
152
+ print(f"{e} DB 연결 실패")
153
+
154
+ def get_style_options():
155
+ try:
156
+ data = style_collection.get(include=["metadatas"], limit=9999)
157
+ names = [m.get("display_name") for m in data["metadatas"] if m.get("display_name")]
158
+ names = sorted(set(names))
159
+
160
+ return ["선택 안 함"] + names
161
+
162
+ except Exception as e:
163
+ print("ERROR in get_style_options:", e)
164
+ return ["선택 안 함"]
165
+
166
+ style_key_options = get_style_options()
167
+
168
+ '''
169
+ 추천 상품의 사이즈와 유사도 점수 가장 높은 상품의 추천 사이즈 간 유클리디언 거리 측정
170
+ (단, 추천 상품의 사이즈 데이터가 하나 밖에 없을 경우 상세 사이즈 수치만 제시)
171
+ '''
172
+ def set_fin_size(id, size_label):
173
+ '''
174
+ [1단계] 유사도 점수가 가장 높은 상품 사이즈의 row 가져오기
175
+ '''
176
+ if prod_size_df.empty:
177
+ return None
178
+
179
+ try:
180
+ r = prod_size_df[
181
+ (prod_size_df['primary_id'] == str(id)) &
182
+ (prod_size_df['size_cleaned'].astype(str) == str(size_label))
183
+ ]
184
+ if r.empty:
185
+ return None
186
+
187
+ numeric_cols = r.select_dtypes(include=[np.number]).columns.tolist()
188
+ numeric_cols = [c for c in numeric_cols if c not in ['primary_id']]
189
+
190
+ return r.iloc[0][numeric_cols].to_dict()
191
+ except:
192
+ return None
193
+
194
+ def find_closest_size(target_id, ideal_spec):
195
+ '''
196
+ [2단계] 추천 상품의 사이즈 중 ideal_spec에 가장 가까운 사이즈 찾기
197
+ '''
198
+ if prod_size_df.empty or not ideal_spec:
199
+ return None
200
+
201
+ target_r = prod_size_df[prod_size_df['primary_id'] == str(target_id)]
202
+ if target_r.empty:
203
+ return None
204
+
205
+ if len(target_r) == 1:
206
+ only_size = target_r.iloc[0]['size_cleaned']
207
+ return only_size
208
+
209
+ if not ideal_spec:
210
+ return None
211
+
212
+ min_dist = float('inf')
213
+ best_size = None
214
+ common_keys = [k for k in ideal_spec.keys() if k in target_r.columns]
215
+
216
+ if not common_keys:
217
+ return None
218
+
219
+ for _, row in target_r.iterrows():
220
+ try:
221
+ c_specs = [float(ideal_spec[k]) for k in common_keys if pd.notna(row[k])]
222
+ t_specs = [float(row[k]) for k in common_keys if pd.notna(row[k])]
223
+
224
+ if not c_specs or len(c_specs) != len(t_specs):
225
+ continue
226
+
227
+ dist = np.linalg.norm(np.array(c_specs) - np.array(t_specs))
228
+ if dist < min_dist:
229
+ min_dist = dist
230
+ best_size = row['size_cleaned']
231
+ except:
232
+ continue
233
+
234
+ return best_size
235
+
236
+ def search_recom_products(brand_lb, cat,
237
+ sel_key, txt, img, user_info,
238
+ min_price, max_price, check_option):
239
+ #=================================================================================
240
+ # 추천 1단계: 프롬프트에 입력된 스타일 값을 바탕으로 벡터 유사도 계산 후 top-5개 추천
241
+ #================================================================================
242
+ query_vec, source = get_search_vector(sel_key, txt, img)
243
+ if query_vec is None:
244
+ return [], "검색 조건을 입력하세요", {}, "", []
245
+
246
+ gender_option = [user_info['gender'], 'all'] # 남성: male, 여성: female, 공용: all nan: all로 바꾸기
247
+
248
+ filter = {
249
+ "$and": [
250
+ {"brand_label": {"$eq": brand_lb}},
251
+ {"sub_category": {"$eq": cat}}
252
+ ]
253
+ }
254
+
255
+ if min_price is not None and max_price is not None:
256
+ if min_price > max_price:
257
+ min_price, max_price = max_price, min_price
258
+ filter["$and"].append({"price": {"$gte": min_price}})
259
+ filter["$and"].append({"price": {"$lte": max_price}})
260
+
261
+ if not check_option:
262
+ filter["$and"].append({"gender": {"$in": gender_option}})
263
+
264
+
265
+ candidates = product_collection.query(
266
+ query_embeddings=[query_vec],
267
+ n_results=20,
268
+ where=filter,
269
+ include=["metadatas", "embeddings", "distances"]
270
+ )
271
+
272
+ if not candidates['ids'][0]:
273
+ return [], "조건에 맞는 상품이 없습니다.", {}, "", []
274
+
275
+ candidate_list_text = ""
276
+ candidate_map = {}
277
+
278
+ for i, meta in enumerate(candidates['metadatas'][0]):
279
+ p_id = meta.get('primary_id')
280
+ name = meta.get('product_name')
281
+ brand = meta.get('brand')
282
+ price = meta.get('price')
283
+ dist = candidates['distances'][0][i]
284
+ sim = 1 / (1 + dist) # 유사도 점수로 변환
285
+
286
+ candidate_list_text += f"[{i+1}] ID: {p_id} | 브랜드: {brand} | 제품명: {name} | 스타일유사도: {sim:.4f}\n"
287
+
288
+ candidate_map[str(i+1)] = {
289
+ "id": p_id, "name": name, "brand": brand, "price": price,
290
+ "cat": meta.get('sub_category'), "brand_lb": meta.get('brand_label'),
291
+ "idx": i
292
+ }
293
+
294
+ print(candidate_map)
295
+ style_requests = []
296
+
297
+ if txt:
298
+ style_requests.append(f"텍스트 요청: '{txt}'")
299
+
300
+ if sel_key and sel_key != '선택 안 함':
301
+ style_key_data = style_collection.get(
302
+ where={"display_name": {"$eq": sel_key}},
303
+ include=["metadatas"]
304
+ )
305
+ if style_key_data["metadatas"]:
306
+ style_desc = style_key_data["metadatas"][0].get("description")
307
+ if style_desc:
308
+ style_requests.append(f"선택 스타일 키워드 설명: '{style_desc}'")
309
+
310
+ if img is not None:
311
+ style_requests.append("이미지 기반 스타일 검색 포함")
312
+
313
+ if style_requests:
314
+ user_input_str = ", ".join(style_requests)
315
+
316
+ # 브랜드 라벨도 추가해서 특정 무드의 브랜드들을 선호하는 소비자의 성향 반영 필
317
+ selection_persona_prompt = PromptTemplate.from_template("""
318
+ 당신은 전문 패션 MD입니다.
319
+ 사용자 프로필과 스타일 유사도 데이터를 바탕으로 선정된 후보 목록 20개를 분석하여, 가장 적합한 **제품 5개를 선정**하고 **패션 페르소나**를 정의하세요.
320
+
321
+ [사용자 프로필]
322
+ - 성별/체형: {g} / {h}cm / {w}kg
323
+ - 선호 스타일 요청: "{s}"
324
+
325
+ [후보 제품 목록 (유사도 순)]
326
+ {c_list}
327
+
328
+ [사고 과정 (Chain of thought)]
329
+ 1. 사용자 분석: 사용자 프로필 정보를 고려하여 사용자의 성별/체형에서 사용자의 선호 스타일을 표현하기 위해서는 어떤 실루엣, 소재, 디자인이 가장 적합한지를 기준을 세우세요.
330
+ 2. 추천 제품 선정: 위에서 세운 기준에 근거하여 사용자에게 가장 적합한 제품 5개를 선정하세요. (단순 유사도 순이 아닌, 스타일 적합성 우선)
331
+ 3. 페르소나 도출: 선별된 5개 제품과 사용자의 특징을 종합하여, 이 사용자가 추구하는 패션 아이덴티티(페르소나)를 구체적인 코디 예시를 들어 묘사합니다.
332
+
333
+ [출력 형식 및 제약 사]
334
+ 1. 사고 과정(설명)은 출력하지 말고, 오직 **결과 데이터**만 아래 형식으로 출력하세요.
335
+ 2. 제품번호는 후보 목록에 표시된 **순번(숫자)**만 적으세요.
336
+
337
+ SELECTED_IDS = [순번1, 순번2, 순번3, 순번4, 순번5]
338
+ |||
339
+ PERSONA = (여기에 3줄 내외의 페르소나 정의 작성)
340
+ """)
341
+
342
+ print("LLM이 제품을 선별 및 페르소나 생성 중...")
343
+ selection_response = llm.invoke(selection_persona_prompt.format(
344
+ g=user_info['gender'], h=user_info['height'], w=user_info['weight'],
345
+ s=user_input_str, c_list=candidate_list_text
346
+ )).content
347
+
348
+ try:
349
+ parts = selection_response.split("|||")
350
+
351
+ ids_text = parts[0]
352
+ selected_idx = re.findall(r'\d+', ids_text)
353
+ selected_idx = [idx for idx in selected_idx if idx in candidate_map]
354
+
355
+ persona_text = parts[1].replace("PERSONA =", "").strip()
356
+
357
+ except Exception as e:
358
+ print(f"파싱 오류: {e}")
359
+
360
+ if not selected_idx:
361
+ selected_idx = list(candidate_map.keys())[:5]
362
+
363
+ print("LLM 선택 응답:", selection_response)
364
+ print("candidate_map keys:", list(candidate_map.keys()))
365
+ print("selected_idx after filtering:", selected_idx)
366
+
367
+ user_info['p'] = persona_text
368
+
369
+ final_recom_item = []
370
+ p_map = {}
371
+
372
+ for idx_str in selected_idx[:5]:
373
+ item_data = candidate_map[idx_str]
374
+ img_url = get_img_url(item_data['id'])
375
+ label = f"[{item_data['brand']}]_{item_data['name']}] - {item_data['price']}원"
376
+
377
+ final_recom_item.append((img_url, label))
378
+ final_recom_item_gr = [[url if url else "", str(label)] for url, label in final_recom_item]
379
+ # Gradio Gallery, Image에 데이터를 넣을 때는 list 형태로 들어가야 됨
380
+ p_map[label] = item_data
381
+
382
+ print("Gallery 데이터:", final_recom_item_gr)
383
+ return final_recom_item_gr, user_info['p'], p_map, user_info
384
+
385
+ def get_sim_products(target_id, category, brand_label=None, limit=5):
386
+ #========================================================================================
387
+ # 추천 2단계: 사이즈 추천 로직
388
+ # (추천 제품과 유사도 높은 제품 구하는 로직: 추천 제품의 리뷰가 임계치 이하일 경우 로직 작동)
389
+ #========================================================================================
390
+ target_data = product_collection.get(
391
+ ids=[target_id], include=['embeddings', 'metadatas'])
392
+
393
+ embs = target_data.get('embeddings')
394
+ if embs is None or len(embs) == 0:
395
+ return {}
396
+
397
+ target_img = torch.tensor(embs[0], dtype=torch.float32)
398
+ try:
399
+ meta_size = target_data['metadatas'][0].get('size_vec', '[]') #오 뭐야 왜 size_vec이 없냐
400
+ target_size = torch.tensor(ast.literal_eval(meta_size), dtype=torch.float32)
401
+ except:
402
+ return {}
403
+
404
+ review_ids_list = list(review_ids)
405
+ filter = {"$and": [{"sub_category": {"$eq": category}},
406
+ {"primary_id": {"$in": review_ids_list}}, # primary_key save_type 문제 flaot타입으로 저장되어서 맞는게 없음
407
+ {"review_count": { "$gt": 0 }}]}
408
+
409
+ if brand_label is not None:
410
+ filter["$and"].append({"brand_label": {"$eq": brand_label}})
411
+
412
+ candidates = product_collection.get(
413
+ where=filter, include=['embeddings', 'metadatas'])
414
+
415
+ cand_ids = candidates.get('ids')
416
+ if cand_ids is None or len(cand_ids) == 0:
417
+ return {}
418
+
419
+ valid_idx, candi_img, candi_size = [], [], []
420
+ for i, p_id in enumerate(cand_ids):
421
+ try:
422
+ v_str = candidates['metadatas'][i].get('size_vec')
423
+ if v_str:
424
+ v = ast.literal_eval(v_str)
425
+ if v:
426
+ candi_size.append(v)
427
+ candi_img.append(candidates['embeddings'][i])
428
+ valid_idx.append(i)
429
+ except:
430
+ continue
431
+
432
+ if not valid_idx:
433
+ return {}
434
+
435
+ cand_img_vec = torch.tensor(np.array(candi_img), dtype=torch.float32)
436
+ cand_size_vec = torch.tensor(np.array(candi_size), dtype=torch.float32)
437
+
438
+ scores = (util.cos_sim(target_img, cand_img_vec)[0] * 0.6) + \
439
+ (util.cos_sim(target_size, cand_size_vec)[0] * 0.4)
440
+
441
+ top_k = torch.argsort(scores, descending=True)[:limit].tolist()
442
+
443
+ result_map = {}
444
+ for i in top_k:
445
+ real_idx = valid_idx[i]
446
+ p_id = cand_ids[real_idx]
447
+ score = float(scores[i])
448
+ result_map[p_id] = score
449
+
450
+ print(result_map)
451
+
452
+ return result_map
453
+
454
+ def recom_size(label, p_map, user_info):
455
+ '''
456
+ 추천 2단계: 사이즈 추천 로직
457
+ '''
458
+ # 유저가 상품 선택 후 선택 상품의 한해 사이즈 추천 진행
459
+ if not label:
460
+ return "상품을 먼저 선택해주세요"
461
+ if label not in p_map:
462
+ return f"오류: 선택한 제품 '{label}'의 데이터를 찾을 수 없습니다.\n(검색을 다시 하거나 다른 제품을 선택해보세요)", ""
463
+
464
+ if user_info is None:
465
+ user_info = {}
466
+ else:
467
+ user_info = dict(user_info)
468
+
469
+ target_id, cat, bl = p_map[label]['id'], p_map[label]['cat'], p_map[label]['brand_lb']
470
+
471
+ # 유저 페르소나의 리뷰와 유사도 계산
472
+ hyde_prompt = PromptTemplate.from_template("""
473
+ 당신은 지금부터 아래 프로필을 가진 사용자가 되어, 방금 구매한 옷에 대해 **만족한 후기**를 작성해야 합니다.
474
+
475
+ [사용자 프로필]
476
+ - 성별: {g}
477
+ - 신체 스펙: {h}cm / {w}kg
478
+ - 사용자 페르소나: {p}
479
+
480
+ [구매한 상품 정보]
481
+ - 제품 기본 정보: {p_info}
482
+ - 카테고리: {cat} (예: 후드티, 와이드 슬랙스)
483
+
484
+ [작성 지침]
485
+ 1. 당신의 신체 스펙과 선호 스타일을 고려하여 사용자가 제품 핏에 만족한다는 가정 하에 리뷰를 작성하세요.
486
+ 2. **주의:** S, M, L 같은 **구체적인 사이즈(옵션명)는 절대 언급하지 마세요.** 오직 '핏감(Fit)'과 '착용감'에 대해서만 이야기하세요.
487
+ 3. 리뷰는 구매한 상품 정보에 기반하여 작성하세요.
488
+ 4. 말투는 자연스러운 한국어 구어체(리뷰톤)를 사용하며 3줄 이내로 작성하세요.
489
+ """)
490
+
491
+ hyde_txt = llm.invoke(hyde_prompt.format(
492
+ h=user_info.get('height',''), w=user_info.get('weight',''),
493
+ g=user_info['gender'], p=user_info.get('p',''),
494
+ p_info=label, cat=cat)).content
495
+ hyde_vec = review_emb_func.embed_query(hyde_txt)
496
+ print(hyde_txt)
497
+
498
+ product_score_map = {str(target_id): 1.0} # 상품 유사도 저장 (해당 추천 상품은 유사도 1)
499
+
500
+ search_pool_ids = [str(target_id)]
501
+ method_log = f"해당 추천 상품"
502
+
503
+ sim_ids_2 = get_sim_products(target_id, cat, brand_label=bl, limit=5)
504
+ if sim_ids_2:
505
+ product_score_map.update(sim_ids_2)
506
+ search_pool_ids.extend(sim_ids_2.keys())
507
+ method_log += f" + 같은 브랜드 그룹 내 유사도 높은 상품 {len(sim_ids_2)}개"
508
+
509
+ if len(search_pool_ids) < 3:
510
+ sim_ids_3 = get_sim_products(target_id, cat, brand_label=None, limit=5)
511
+
512
+ # 중복 제거
513
+ for id, score in sim_ids_3.items():
514
+ if id not in product_score_map:
515
+ product_score_map[id] = score
516
+ search_pool_ids.extend(sim_ids_2.keys())
517
+ method_log = f" + 같은 카테고리 내 유사도 높은 상품 {len(sim_ids_3)}개"
518
+
519
+ gender = user_info.get('gender', 'male')
520
+ height = int(user_info.get('height'))
521
+ weight = int(user_info.get('weight'))
522
+
523
+ min_h, max_h = height - 3, height + 3
524
+ min_w, max_w = weight - 3, weight + 3
525
+
526
+ final_docs_raw = review_db.similarity_search_by_vector(
527
+ hyde_vec,
528
+ k=30,
529
+ filter={
530
+ "$and": [
531
+ {"product_id": {"$in": search_pool_ids}}, # 리뷰 데이터 primary_id vectorDB 저장 시 float --> int로 타입 변환 후 str 씌우기!!!!!!!!
532
+ {"gender": {"$eq": gender}},
533
+ {"height": {"$gte": min_h}},
534
+ {"height": {"$lte": max_h}},
535
+ {"weight": {"$gte": min_w}},
536
+ {"weight": {"$lte": max_w}}
537
+ ]
538
+ })
539
+
540
+ if not final_docs_raw:
541
+ relaxed_filter = {
542
+ "$and": [
543
+ {"product_id": {"$in": search_pool_ids}},
544
+ {"gender": {"$eq": gender}}
545
+ ]
546
+ }
547
+ final_docs_raw = review_db.similarity_search_by_vector(hyde_vec, k=30, filter=relaxed_filter) # 리스트 타입 반환 [doc1, doc2, doc3...]
548
+
549
+ if not final_docs_raw:
550
+ return "유사 리뷰 데이터 부족", method_log
551
+
552
+ print(final_docs_raw)
553
+ best_doc, max_score, weight_docs = None, -1e9 , []
554
+ size_score = defaultdict(float)
555
+ size_count = defaultdict(int)
556
+
557
+ valid_doc_cnt = 0
558
+
559
+ for doc in final_docs_raw:
560
+ meta = getattr(doc, 'metadata', {}) or {}
561
+ size = meta.get('size')
562
+ if size:
563
+ size_count[size] += 1
564
+ valid_doc_cnt += 1
565
+ else:
566
+ continue
567
+ if valid_doc_cnt == 0:
568
+ return "best_doc is not found", method_log
569
+
570
+ for doc in final_docs_raw:
571
+ meta = getattr(doc, 'metadata', {}) or {}
572
+ prod_id = meta.get('product_id')
573
+ size = meta.get('size')
574
+ #print("DOC META:", meta) # 디버깅용 코드
575
+ #print("prod_id:", prod_id, 'size:', size)
576
+
577
+ if not prod_id or not size:
578
+ continue
579
+
580
+ dis = meta.get("score", 0.0) # 상품 유사도
581
+ review_sim = max(0.0, 1.0 - dis) # 리뷰 유사도
582
+ s_cnt = size_count[size]
583
+ size_ratio_score =(1 + s_cnt / valid_doc_cnt)
584
+
585
+ prod_id = str(prod_id)
586
+ prod_sim = float(product_score_map.get(prod_id, 0.5)) # 상품 유사
587
+ w_score = prod_sim * review_sim * size_ratio_score # 가중 점수에 (추천 리뷰 사이즈 개수 / 전체 사이즈 개수)까지 곱해주기
588
+
589
+ weight_docs.append({
590
+ "doc": doc,
591
+ "id": prod_id,
592
+ "weight": w_score,
593
+ "size": size
594
+ })
595
+
596
+ size_score[size] += w_score
597
+
598
+ if w_score > max_score:
599
+ max_score = w_score
600
+ best_doc = {
601
+ "product_id": prod_id,
602
+ "size": size
603
+ }
604
+
605
+ if not best_doc:
606
+ return "best_doc is not found", method_log
607
+
608
+ ref_p, ref_sz = best_doc['product_id'], best_doc['size']
609
+ ideal_spec = set_fin_size(ref_p, ref_sz) # 가장 유사한 제품 사이즈 정보 가져오기
610
+
611
+ final_sz, info = ref_sz, "(유사 제품 사이즈 추천)"
612
+
613
+ # 추천 상품 사이즈 옵션 개수 확인
614
+ target_r = prod_size_df[prod_size_df['primary_id'] == str(target_id)]
615
+ size_cnt = len(target_r)
616
+
617
+ if size_cnt == 1:
618
+ final_sz = target_r.iloc[0]['size_cleaned']
619
+ info = "(원사이즈 제품)"
620
+ elif ideal_spec:
621
+ actual_sz = find_closest_size(target_id, ideal_spec)
622
+ if actual_sz:
623
+ final_sz = actual_sz
624
+ info = f"(유사 제품 {ref_p} [{ref_sz}] 실측 기반 매핑)"
625
+
626
+ weight_docs.sort(key=lambda x: x['weight'], reverse=True)
627
+
628
+ evidence_txt = '\n'.join([
629
+ f"-[{doc['size']}] {doc['doc'].page_content[:80]}...({doc['weight']:.2f})"
630
+ for doc in weight_docs[:5]
631
+ ])
632
+
633
+ detail_log = "\n".join([
634
+ f"[{i+1}위] ID: {d['id']} ({d['size']}) --> {d['weight']:.2f}점"
635
+ for i, d in enumerate(weight_docs[:10])
636
+ ])
637
+
638
+ score_dist = ", ".join([
639
+ f"{k}: {v:.1f}" for k, v in sorted(size_score.items(), key=lambda x:x[1], reverse=True)
640
+ ])
641
+
642
+ # 추천 근거 생성 LLM (상세 사이즈 정보 추가)
643
+ prompt = "패션 상품 구매를 고려하고 있는 사용자에게 '{s}' 사이즈 추천. 근거:\n{e}\n페르소나: {p}\n(로직: {m}, {i})"
644
+ msg = llm.invoke(prompt.format(
645
+ s=final_sz, e=evidence_txt, p=user_info.get('p',''), m=method_log, i=info)).content
646
+
647
+ return final_sz, msg
648
+
649
+ # 이미지 url 가져오기
650
+ def get_img_url(id):
651
+ if prod_data.empty:
652
+ return None
653
+
654
+ try:
655
+ r = prod_data[prod_data['primary_id'] == str(id).strip()]
656
+ if not r.empty:
657
+ url = r.iloc[0]['thumbnail_img_url']
658
+ if pd.notna(url):
659
+ return str(url)
660
+ except:
661
+ pass
662
+ return None
663
+
664
+ # 검색 벡터 생성
665
+ def get_search_vector(selected_key, user_txt, img_path):
666
+ vector_list = [] # input_prompt 요소들의 embedding_vector 리스트
667
+ message_list = []
668
+
669
+ if img_path:
670
+ image = Image.open(img_path)
671
+ inputs = clip_processor(images=image, return_tensors="pt", padding=True).to(device)
672
+ with torch.no_grad():
673
+ img_vec = clip_model.get_image_features(**inputs).cpu().numpy().tolist()[0]
674
+ vector_list.append(img_vec)
675
+ message_list.append("이미지 기반 검색")
676
+
677
+ if user_txt:
678
+ inputs = clip_processor(text=[user_txt], return_tensors="pt", padding=True).to(device)
679
+ with torch.no_grad():
680
+ txt_vec = clip_model.get_text_features(**inputs).cpu().numpy().tolist()[0]
681
+ vector_list.append(txt_vec)
682
+ message_list.append(f"{user_txt}에 대한 검색 중...")
683
+
684
+ if selected_key and selected_key != '선택 안 함':
685
+ items = style_collection.get(include=["metadatas", "embeddings"])
686
+ query_vec = None
687
+
688
+ for m, e in zip(items["metadatas"], items["embeddings"]):
689
+ if m.get("display_name") == selected_key:
690
+ vector_list.append(e)
691
+ message_list.append(f"{selected_key}에 대한 검색 중...")
692
+ break
693
+
694
+ if not vector_list:
695
+ return None, "검색 조건을 입력하세요"
696
+ else:
697
+ mean_vector = torch.mean(torch.tensor(vector_list), dim=0).tolist() # 멀티모달 사용 시 벡터 concat 후 512차원으로 projection
698
+ return mean_vector, ", ".join(message_list)
699
+
700
+ def on_gallery_click(p_map, evt: gr.SelectData):
701
+ """
702
+ 갤러리 클릭 시 해당 인덱스의 제품 라벨(Key)을 찾아 반환하는 함수
703
+ """
704
+ if not p_map:
705
+ return ""
706
+
707
+ keys_list = list(p_map.keys())
708
+
709
+ if evt.index < len(keys_list):
710
+ selected_label = keys_list[evt.index]
711
+ return selected_label # 텍스트 박스에 들어갈 라벨 반환
712
+
713
+ return ""
714
+
715
+ # 브랜드 label 선택 시 category range 변경
716
+ def update_cat(brand_label):
717
+ if not brand_label:
718
+ return gr.Dropdown(choices=[], value=None)
719
+
720
+ try:
721
+ filtered_df = prod_data[prod_data['brand_label'] == brand_label]
722
+ new_choices = sorted(filtered_df['category_final'].dropna().unique().tolist())
723
+
724
+ new_value = new_choices[0] if new_choices else None
725
+ return gr.Dropdown(choices=new_choices, value=new_value, label="카테고리 (서브)")
726
+
727
+ except Exception as e:
728
+ print(f" 카테고리 업데이트 실패: {e}")
729
+ return gr.Dropdown(choices=[], value=None)
730
+
731
+ # 카테고리 선택 시 가격 range 변경 함수
732
+ def update_price(brand_label, cat):
733
+ min_price = int(prod_data['sale_price'].dropna().min())
734
+ max_price = int(prod_data['sale_price'].dropna().max())
735
+
736
+ if not brand_label or not cat:
737
+ return (f"가격범위: {min_price:,}원 ~ {max_price:,}원",
738
+ min_price, max_price,
739
+ gr.update(visible=False)
740
+ )
741
+
742
+ try:
743
+ selected_prod_df = prod_data[(prod_data['brand_label'] == brand_label) & (prod_data['category_final'] == cat)]
744
+ update_min_price = int(selected_prod_df['sale_price'].min())
745
+ update_max_price = int(selected_prod_df['sale_price'].max())
746
+
747
+ price_text_info = f"가격범위: {update_min_price:,}원 ~ {update_max_price:,}원"
748
+
749
+ return price_text_info, update_min_price, update_max_price, gr.update(visible=False)
750
+
751
+ except Exception as e:
752
+ print(f"가격 업데이트 실패: {e}")
753
+ return min_price, max_price
754
+
755
+ brand_description = {
756
+ "미니멀리즘, 높은 퀄리티, 편안함, 타임리스, 웨어러블":
757
+ "미니멀하고 타임리스한 디자인을 통해 높은 품질의 편안한 의류를 제공하며, 실용성과 웨어러블함을 중요시합니다.",
758
+ "편안함, 스타일, 개성, 실용성, 자유로움":
759
+ "이 클러스터의 브랜드들은 편안한 실루엣과 실용적인 디자인을 통해 개인의 개성을 표현하며, 일상에서 자유로운 스타일을 추구합니다.",
760
+ "편안함, 일상, 유연함, 트렌디, 개인성":
761
+ "일상 속에서 편안하게 착용할 수 있는 트렌디한 디자인을 추구하며, 개인의 개성과 다양한 라이프스타일을 반영하는 브랜드들이 모여 있습니다.",
762
+ "편안함, 유니크함, 지속 가능성, 반항적 디자인, 일상성과 특별함":
763
+ "이 클러스터의 브랜드들은 일상 속에서 편안함과 유니크함을 추구하며, 반항적이고 독창적인 디자인으로 새로운 흐름을 제안하는 동시에 지속 가능한 삶을 지향합니다.",
764
+ "편안함, 개성, 자연스러움, 균형, 다채로움":
765
+ "이 클러스터의 브랜드들은 편안함과 개성을 중시하며, 자연스러운 스타일과 균형 잡힌 라이프스타일을 통해 다양성을 존중하는 독창적인 패션을 제안합니다.",
766
+ "아웃도어, 기능성, 일상, 스포츠, 혁신":
767
+ "스포츠와 아웃도어 활동을 일상 속에서 즐길 수 있도록 기능성과 스타일을 겸비한 다양한 제품을 제공하는 브랜드들이 모인 클러스터입니다.",
768
+ "여성, 클래식, 트렌디, 웨어러블, 심플, 감성":
769
+ "이 클러스터의 브랜드들은 클래식함과 트렌디함을 조화롭게 담아내며, 편안한 착용감을 제공하는 웨어러블한 디자인과 감성적인 스타일을 강조합니다."
770
+
771
+ }
772
+ def load_stye_collection():
773
+ data = style_collection.get(include=["metadatas"], limit=9999)
774
+
775
+ desc_dict = {}
776
+ for meta in data['metadatas']:
777
+ style_key = meta.get('display_name')
778
+ style_desc = meta.get('description')
779
+
780
+ if style_key and style_desc and style_key not in desc_dict:
781
+ desc_dict[style_key] = style_desc
782
+
783
+ return desc_dict
784
+
785
+ style_description = load_stye_collection()
786
+
787
+ brand_label_choice, subcat_choice = [], []
788
+ min_price, max_price = 0, 100
789
+
790
+ if not prod_data.empty:
791
+ brand_label_choice = sorted(prod_data['brand_label'].dropna().unique().tolist())
792
+ subcat_choice = sorted(prod_data['category_final'].dropna().unique().tolist())
793
+ min_price = int(prod_data['sale_price'].dropna().min())
794
+ max_price = int(prod_data['sale_price'].dropna().max())
795
+
796
+ # 데이터에 맞는 변수명으로 변경 작업
797
+ def get_brand_desc(brand_label):
798
+ return brand_description.get(brand_label, "")
799
+
800
+ def get_style_desc(style):
801
+ return style_description.get(style, "")
802
+
803
+ # [1] 텍스트를 예쁜 HTML 박스로 감싸주는 함수
804
+ def format_info_box(text):
805
+ if not text:
806
+ return ""
807
+ # 여기에 style을 직접 적으면 절대 무시당하지 않습니다.
808
+ return f"""
809
+ <div style="
810
+ background-color: #f0f9ff;
811
+ color: #333333;
812
+ border: 1px solid #bae6fd;
813
+ border-radius: 8px;
814
+ padding: 12px;
815
+ text-align: center;
816
+ font-weight: 600;
817
+ font-size: 0.95em;
818
+ margin-top: 5px;
819
+ ">
820
+ {text}
821
+ </div>
822
+ """
823
+
824
+ with gr.Blocks(theme=gr.themes.Soft()) as app:
825
+ state_pmap = gr.State({}) # 제품 메타데이터 저장
826
+ state_results = gr.State({})
827
+
828
+ with gr.Group(visible=True) as page_input:
829
+ gr.Markdown("### 1. 스타일 조건을 입력해주세요")
830
+ with gr.Column():
831
+ in_g = gr.Radio(["male", "female"], value="male", label="성별")
832
+ check = gr.Checkbox(label="다른 성별의 제품도 볼래요", value=False)
833
+
834
+ with gr.Row():
835
+ in_h, in_w = gr.Number(175, label="키"), gr.Number(70, label="몸무게")
836
+
837
+ with gr.Row():
838
+ with gr.Column():
839
+ in_bg = gr.Dropdown(brand_label_choice, value=brand_label_choice[0] if brand_label_choice else None,
840
+ label="브랜드 그룹", info="선택 시 브랜드 설명이 아래에 나타납니다.")
841
+ initial_bg_text = get_brand_desc(brand_label_choice[0] if brand_label_choice else "")
842
+ bg_desc = gr.HTML(value=format_info_box(initial_bg_text), visible=True)
843
+
844
+ in_cat = gr.Dropdown(subcat_choice, value=subcat_choice[0] if subcat_choice else None, label="카테고리")
845
+
846
+ price_info = gr.Markdown(value=f"가격범위: {min_price:,}원 ~ {max_price:,}원")
847
+ with gr.Row():
848
+ min_num = gr.Number(value=min_price, label="최소 가격 입력")
849
+ max_num = gr.Number(value=max_price, label="최대 가격 입력")
850
+ price_error = gr.Markdown("", visible=False)
851
+
852
+ with gr.Column():
853
+ in_sel = gr.Dropdown(choices = style_key_options, value="선택 안 함",
854
+ label="스타일", info="원하는 스타일 무드를 선택해보세요.")
855
+ style_desc = gr.HTML(value=format_info_box(""), visible=True)
856
+
857
+ in_txt = gr.Textbox(label="텍스트 (패션 추구미를 직접 입력해보세요)")
858
+ in_img = gr.Image(type="filepath", label="이미지를 넣으면 더 정밀한 추천을 받을 수 있어요 (선택 사항)")
859
+
860
+ btn_s = gr.Button("🔍 검색", variant='primary', size='lg')
861
+
862
+ # 검색 결과 페이지
863
+ with gr.Group(visible=False) as page_result:
864
+ gr.Markdown("### 2. 추천 결과")
865
+ out_p = gr.Textbox(label="나만의 패션 페르소나", lines=3)
866
+
867
+ with gr.Row():
868
+ with gr.Column(scale=1):
869
+ gal = gr.Gallery(label="검색 결과", columns=3)
870
+
871
+ with gr.Column(scale=2):
872
+ sel = gr.Textbox(label="사이즈 추천을 원하는 상품")
873
+ btn_r = gr.Button("사이즈 추천")
874
+ f_sz = gr.Textbox(label="추천 사이즈", lines=1)
875
+ out_m = gr.Textbox(label="추천 이유 설명", lines=5, max_lines=10)
876
+
877
+ with gr.Row():
878
+ btn_back = gr.Button("검색 조건 다시 입력하기", variant='secondary')
879
+
880
+ def validate_price(min_num, max_num):
881
+ if min_num < min_price:
882
+ return gr.update(value=f"최소 가격은 {min_price:,}원 이상이어야 합니다.", visible=True)
883
+ if max_num > max_price:
884
+ return gr.update(value=f"최대 가격은 {max_price:,}원 이하이어야 합니다.", visible=True)
885
+ if min_num > max_num:
886
+ return gr.update(value="최소 가격이 최대 가격보다 클 수 없습니다.", visible=True)
887
+
888
+ return gr.update(value="", visible=False)
889
+
890
+ min_num.change(fn=validate_price, inputs=[min_num, max_num], outputs=[price_error])
891
+ max_num.change(fn=validate_price, inputs=[min_num, max_num], outputs=[price_error])
892
+
893
+ in_bg.change(fn=lambda x: format_info_box(get_brand_desc(x)),
894
+ inputs=[in_bg],
895
+ outputs=[bg_desc])
896
+ in_sel.change(fn=lambda x: format_info_box(get_style_desc(x)),
897
+ inputs=[in_sel],
898
+ outputs=[style_desc])
899
+
900
+ in_bg.change(fn=update_cat, inputs=[in_bg], outputs=[in_cat])
901
+ in_cat.change(fn=update_price, inputs=[in_bg, in_cat], outputs=[price_info, min_num, max_num, price_error])
902
+
903
+ # 페이지 전환 함수
904
+ def move_to_result():
905
+ return gr.update(visible=False), gr.update(visible=True)
906
+
907
+ def move_to_input():
908
+ return gr.update(visible=True), gr.update(visible=False)
909
+
910
+ def search_and_log(bg, c, sl, tx, im, g, h, w, min_p, max_p, check):
911
+ try:
912
+ gr.Info("🚀 사용자 데이터 분석을 시작합니다...")
913
+
914
+ if not bg or not c:
915
+ raise gr.Error("브랜드그룹과 카테고리를 반드시 선택하세요")
916
+
917
+ # 1. 기존 검색 로직 수행
918
+ user_info = {'gender': g, 'height': h, 'weight': w}
919
+
920
+ results = search_recom_products(bg, c, sl, tx, im, user_info, min_p, max_p, check)
921
+
922
+ gr.Info("✅ 분석 완료! 결과 페이지로 이동합니다.")
923
+ persona_result = results[1]
924
+
925
+ try:
926
+ # 2. DB에 입력값과 결과 저장
927
+ log_search_to_sheet(bg, c, min_p, max_p, sl, tx, check, persona_result)
928
+ except Exception as log_e:
929
+ print(f"로그 저장 실패: {log_e}")
930
+
931
+ return results
932
+
933
+ except gr.Error as ge:
934
+ raise ge
935
+
936
+ except Exception as e:
937
+ print(f"시스템 에러: {e}")
938
+ raise gr.Error(f"시스템 오류가 발생했습니다: {str(e)}")
939
+
940
+ def size_recom_and_log(sel_product, p_map, u_info):
941
+ # 1. 기존 사이즈 추천 로직 수행
942
+ # recom_size 함수는 2개의 값을 반환한다고 가정 (f_sz, out_m)
943
+ rec_size, reason = recom_size(sel_product, p_map, u_info)
944
+
945
+ # 2. DB에 저장
946
+ log_size_to_sheet(sel_product, u_info, rec_size, reason)
947
+
948
+ return rec_size, reason
949
+
950
+ # 1. 검색 버튼 연결
951
+ btn_s.click(
952
+ fn=search_and_log,
953
+ inputs=[in_bg,in_cat,in_sel,in_txt,in_img,in_g,in_h,in_w, min_num, max_num, check],
954
+ outputs=[gal, out_p, state_pmap, state_results]
955
+ ).then(
956
+ fn=move_to_result,
957
+ inputs=None,
958
+ outputs=[page_input, page_result]
959
+ ) # 버튼 클릭 시 페이지 이동
960
+
961
+ btn_back.click( # 버튼 클릭 시 페이지 이동
962
+ fn=move_to_input,
963
+ inputs=None,
964
+ outputs=[page_input, page_result]
965
+ )
966
+
967
+ gal.select(fn=on_gallery_click, inputs=[state_pmap], outputs=sel)
968
+
969
+ # 3. 사이즈 추천 버튼 연결
970
+ # inputs에 state_persona를 넣어서 recommend_size로 전달
971
+ btn_r.click(
972
+ fn=size_recom_and_log,
973
+ inputs=[sel, state_pmap, state_results], # -> state_persona 값을 꺼내서 씀
974
+ outputs=[f_sz, out_m]
975
+ )
976
+
977
+ try:
978
+ app.close()
979
+ except:
980
+ pass
981
+
982
+ app.queue().launch(share=True)
fashion_key.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "type": "service_account",
3
+ "project_id": "fluent-music-480402-m3",
4
+ "private_key_id": "204a19a89bde0b461b35ddd5149b6d34e0e955c6",
5
+ "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDuBlEcoMBWT62A\nPX7cX6e/vYYF2Z3F/Bh+TmM5B8l3yHUU/fAiDwV2abb/TYAPPcUXu/ElVwkL8GBl\n9uBRV6zc3GsndR9Vu7PQ1j4pt4BKSNhwQPWcmxHhiqET52G5+EDET0SyXD6xjgq8\nAbMFuzkqbtIMUDIo27MND0I47bCerxSCjIyBSKe7faJ69WX7z+xGoZXyuND5Ln4D\n2jyt+vCV9nrX1MkaHMkN9SfwvSh/SlZT7VkbekgjtGK2lQW8VR9QpqcZAGxq/BVt\n7CUghQdB/0efO5DQqgYzPIYiBoSxazH7t13igDZcNeSODcDOxPmAsSRnjK1gtH8n\nB/01HZETAgMBAAECggEAAR0ywElnJCrxrEd3eq/VQt5Vxnlh55anu7rd5fLOa0Kp\nTJcJ5vyJYe89xUlR3wLC7Z5TNFI0Cgs6pLhrdWVp9AHxbr707JbDzHgidcG/X+V8\nRr3IR1Nw/4oP04j8TOz5WO0hm+wadT0aqRdZ1XN6XMvQbyA+5Cb0RzclzvDsFSYx\nSMEICHJ4+q6gi4y42J6b7uw/9kPHhNuVJjBZ9aVDChdLqHvRfIRFxOS/15FlfMyL\n+10gu3iWX1jK975+/wBOQ2fe6RcZ+uLiE0hQ4HGzSZTQLtaf++Vx7K/l0+qMdOMJ\n6sVkuNe40BEij7MOV/2zzC7dQFAIAmyfTsPAEUyBkQKBgQD9wfSrg6rDokdnlJee\nq3p/MVb7DA21G6BEGaq64NN7RWOh98fT+gfHFKI3B7qCGWiSU3Boa5xAylplDHv/\n1ZbKYN8Oj0EA1XHSwYwVMBVfcpDKaCr1kCfvLcvNRQxoQMVc9LIZxPQymGhBn9U3\n1ZTyzfO6l12TDpjq8I2vafy9hwKBgQDwIMU3ewAL77/es3svGqId+OLvWf6fsS0Z\nlnbZGExJxs34xgc7sUDJHV2UGX7FVKrF/jOqBUDTSfmI/kAn85ETF0uBO7hIeuqz\nuqNwDfAIugT45nTPzCS0Xl0BGf0zOnsvUibjC462mm1KM4g/dcep4I2aeR7yMJFy\nmW94gGcTFQKBgDL4fpjk/awGMKmRo1Lvxs89recHaMl0HyEOtsiK2G/uZDDogG8H\nzgAjGOJM3x261NJviCixVNV+z+F0PIQK9xr3klZuV/Q+63oV/nlTCzf83zCI8/gX\nWK4mWMWGlRNrzzj4vb2HXjW7f1WPMVjhweVzkP1t4hHj4LtwTkVp2KaJAoGASp4U\nSqo5GHx2xfBF7hw7lk65ziDMA7mdxx6/bbSkOCTD6nM22jC5bgVnB9doUk9+Bap5\nSXL6cu3A0fYX35JLWYBaoRz6peM0PYdYtEBQf5W2Z9XNJKlOGpJcGjSGPr4Ee27u\n0IzN2yZfobgjtXyW/83ckszideXrvI27WtmjrgUCgYEAiyg/gtAki5HAWG1bsa9p\ndB4yXdW9a7izkWL3QHCodkpF9syQzvvZDK4aP28T4mF0/SxS0aZEj8ZXo7S2en41\nvAvk+iC3qObkQelJzLbLb5imLxoA9n9nHUjQG6ENJtym7eUwrsHjW+a8RKatmR2x\n36GyilDuARXOVLi6zE6x9vA=\n-----END PRIVATE KEY-----\n",
6
+ "client_email": "fashion-recom@fluent-music-480402-m3.iam.gserviceaccount.com",
7
+ "client_id": "107965914755558240659",
8
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
9
+ "token_uri": "https://oauth2.googleapis.com/token",
10
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/fashion-recom%40fluent-music-480402-m3.iam.gserviceaccount.com",
12
+ "universe_domain": "googleapis.com"
13
+ }
final_prod_data.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cf0b50de02abf738157423ea2bf1cd5de772e10a53993886d464b1f5224cb7fa
3
+ size 17593097
final_review_df.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:05bd48bfd2fdb8e083d69fdba498ca9b957835410867c777c05f86b6e48371d5
3
+ size 21556328
prod_emb_data.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ec053f98d4d23f2769d1ef7f47edfa658426ea87bf831ed84fd105b2a033db4c
3
+ size 160038091
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ pandas
3
+ numpy
4
+ torch
5
+ chromadb
6
+ pysqlite3-binary
7
+ transformers
8
+ langchain
9
+ langchain-community
10
+ langchain-huggingface
11
+ langchain-openai
12
+ openai
13
+ sentence-transformers
14
+ gspread
15
+ oauth2client
16
+ pillow
size_sorted.csv ADDED
The diff for this file is too large to render. See raw diff
 
style_key_data.csv ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ style_key,display_name,synonyms,description,embedding_queries_en,embedding_queries_ko
2
+ clean_girl_aesthetic,클린걸 에스테틱,"['clean girl aesthetic', '클린걸룩', '클린룩', '꾸안꾸 미니멀']","깔끔하고 미니멀하며 자연스러운 아름다움을 강조하는 스타일. 뉴트럴 톤, 심플한 실루엣, 정돈된 헤어와 메이크업이 특징. (예: 화이트 티셔츠 + 스트레이트 데님 + 골드 주얼리)","['clean girl aesthetic', 'minimalist natural look', 'effortless chic fashion', 'neutral tones outfit', 'simple silhouette style']","['클린걸 에스테틱', '꾸안꾸 미니멀룩', '자연스러운 스타일', '뉴트럴톤 코디', '심플한 실루엣']"
3
+ vanilla_girl_aesthetic,바닐라걸 에스테틱,"['vanilla girl aesthetic', '바닐라걸룩', '소프트걸', '크림룩']","부드럽고 포근한 느낌을 주는 스타일. 크림, 베이지, 화이트 등 밝고 따뜻한 색감과 니트, 코튼 등 부드러운 소재를 주로 사용. (예: 아이보리 니트 스웨터 + 새틴 스커트)","['vanilla girl aesthetic', 'soft cozy fashion style', 'cream and beige outfits', 'light academia soft', 'warm and comfortable look']","['바닐라걸 에스테틱', '부드럽고 포근한 룩', '크림색 베이지색 코디', '소프트걸 스타일', '따뜻한 느낌의 패션']"
4
+ dark_romantic,다크 로맨틱,"['dark romanticism', 'gothic romantic', '다크 로맨틱룩']","로맨틱한 실루엣에 어둡고 고딕적인 요소를 결합한 스타일. 레이스, 프릴, 코르셋 디테일과 블랙, 버건디 등 깊은 색상을 사용. (예: 블랙 레이스 드레스 + 벨벳 초커)","['dark romantic fashion', 'gothic romantic style', 'victorian goth look', 'lace and velvet dark aesthetic', 'moody romantic outfit']","['다크 로맨틱 패션', '고딕 로맨틱 스타일', '빅토리안 고스룩', '레이스 벨벳 어두운 코디', '분위기 있는 로맨틱룩']"
5
+ indie_sleaze,인디 슬리즈,"['indie sleaze aesthetic', '2000s indie', '인디 슬리즈룩']","2000년대 후반~2010년대 초반 인디 씬의 자유분방하고 다소 정돈되지 않은 스타일. 빈티지, 그런지, 일렉트로 팝 문화를 혼합. (예: 찢어진 스키니진 + 밴드 티셔츠 + 가죽 재킷)","['indie sleaze fashion', '2000s indie aesthetic', 'grunge party look', 'messy chic style', 'vintage band shirt outfit']","['인디 슬리즈 패션', '2000년대 인디 스타일', '그런지 파티룩', '자유분방한 빈티지', '밴드 티셔츠 코디']"
6
+ genderless,젠더리스,"['genderless fashion', 'unisex fashion', '젠더리스룩', '유니섹스']","성별의 경계를 허무는 스타일. 남성복과 여성복의 구분이 없는 오버사이즈 핏, 중성적인 디자인의 아이템을 활용. (예: 오버사이즈 셔츠 + 와이드 슬랙스)","['genderless fashion style', 'unisex clothing', 'androgynous look', 'breaking gender norms in fashion', 'oversized neutral outfit']","['젠더리스 패션', '유니섹스 의류', '중성적인 룩', '성별 구분 없는 스타일', '오버사이즈 중성적인 코디']"
7
+ deconstructed_fashion,해체주의 패션,"['deconstructed clothing', 'deconstructionism', '해체주의룩']","의복의 기존 구조를 의도적으로 해체하고 재조립하여 새로운 형태를 만드는 아방가르드한 스타일. 비대칭, 노출된 시접, 미완성된 듯한 디테일이 특징. (예: 비대칭 헴라인 스커트 + 재구성된 셔츠)","['deconstructed fashion', 'deconstructionism in clothing', 'asymmetrical garment design', 'raw edges unfinished look', 'avant-garde reconstruction']","['해체주의 패션', '의류 해체주의', '비대칭 디자인', '노출된 시접 마감', '아방가르드 재구성']"
8
+ avant_garde,아방가르드,"['avant-garde fashion', 'experimental fashion', '전위적인 패션']","전통적인 패션의 관습과 규범을 깨는 실험적이고 혁신적인 스타일. 비정형적인 실루엣, 독특한 소재, 예술적인 표현을 중시. (예: 과장된 볼륨의 구조적인 드레스)","['avant-garde fashion style', 'experimental clothing', 'unconventional silhouettes', 'artistic fashion', 'pushing fashion boundaries']","['아방가르드 패션', '실험적인 의상', '비정형적 실루엣', '예술적인 패션', '관습을 깨는 디자인']"
9
+ futuristic_fashion,퓨처리스틱,"['futuristic style', 'sci-fi fashion', '미래지향적 패션', '사이버펑크']","미래나 과학 소설(SF)에서 영감을 받은 스타일. 메탈릭 소재, 기하학적 패턴, 구조적인 실루엣, 테크웨어 요소를 특징으로 함. (예: 실버 메탈릭 패딩 점퍼 + 테크 팬츠)","['futuristic fashion', 'sci-fi inspired clothing', 'metallic outfits', 'cyberpunk style', 'techwear aesthetic']","['퓨처리스틱 패션', 'SF 영화 패션', '메탈릭 소재 코디', '사이버펑크 스타일', '테크웨어']"
10
+ goth,고스,"['goth style', 'gothic fashion', '고스룩', '고딕']","어둡고 신비로우며 종종 으스스한 분위기를 연���하는 스타일. 주로 블랙 컬러를 사용하며, 레이스, 가죽, 벨벳 소재와 코르셋, 플랫폼 부츠 등을 활용. (예: 올블랙 코디 + 가죽 부츠 + 실버 액세서리)","['goth fashion style', 'all black gothic look', 'dark aesthetic clothing', 'lace and leather goth', 'platform boots outfit']","['고스 패션 스타일', '올블랙 고딕룩', '어두운 패션', '레이스 가죽 고스', '플랫폼 부츠 코디']"
11
+ emo,이모,"['emo style', 'emo fashion', '이모룩', '이모코어']","이모(Emo) 펑크 음악 하위문화에서 파생된 스타일. 감정적인 표현을 중시하며, 스키니 진, 밴드 티셔츠, 스터드 벨트, 짙은 아이라이너가 특징. (예: 블랙 밴드 티셔츠 + 블랙 스키니진 + 컨버스)","['emo fashion style', 'emotional hardcore fashion', 'skinny jeans and band shirt', 'studded belt aesthetic', '2000s emo look']","['이모 패션 스타일', '이모코어 패션', '스키니진 밴드 티셔츠', '스터드 벨트', '2000년대 이모룩']"
12
+ mod_style,모드,"['mod fashion', '1960s mod', '모드룩', '60년대 스타일']","1960년대 영국 런던에서 시작된 모던(Modern) 스타일. 깔끔하고 대담한 실루엣, 기하학적 패턴, 미니 스커트, A라인 드레스가 특징. (예: A라인 미니 드레스 + 앵클 부츠)","['mod fashion style', '1960s London look', 'A-line mini dress', 'geometric patterns clothing', 'Twiggy style']","['모드 패션 스타일', '1960년대 런던 패션', 'A라인 미니 드레스', '기하학적 패턴', '트위기 스타일']"
13
+ gorpcore,고프코어,"['gorpcore', '고프코어룩', '아웃도어룩', '바람막이 코디']","아웃도어 활동을 위한 기능성 의류(바람막이, 등산화, 카고 팬츠)를 일상복으로 활용하는 스타일. 'Gorp'는 등산객이 먹는 견과류 믹스를 의미.","['gorpcore aesthetic', 'outdoor functional wear as daily fashion', 'man wearing a windbreaker and cargo pants', 'hiking boots street style', 'arcteryx style jacket']","['고프코어 스타일', '기능성 아웃도어 데일리룩', '바람막이와 카고 팬츠 코디', '등산화 스트릿 패션', '아크테릭스 스타일 자켓']"
14
+ balletcore,발레코어,"['balletcore', '발레코어룩', '발레리나 스타일', '리본 패션']","발레리나의 연습복에서 영감을 받은 스타일. 발레 플랫 슈즈, 레그 워머, 튤 스커트(샤 스커트), 리본, 랩 가디건 등이 핵심 아이템.","['balletcore aesthetic', 'ballerina inspired fashion', 'wearing ballet flats and leg warmers', 'tulle skirt with wrap cardigan', 'outfit with pink ribbons']","['발레코어 스타일', '발레리나 패션', '발레 플랫 슈즈와 레그 워머 코디', '튤 스커트와 랩 가디건', '핑크 리본 디테일 아웃핏']"
15
+ blokecore,블록코어,"['blokecore', '블록코어룩', '축구 유니폼 코디', '사커 저지']","영국 남성(Bloke)들이 축구 유니폼(저지)을 일상복처럼 입는 스타일. 축구 유니폼 상의에 데님 팬츠나 트랙 팬츠, 스니커즈(주로 아디다스 삼바)를 매치.","['blokecore aesthetic', 'soccer jersey street style', 'wearing football shirt with jeans', 'adidas samba sneakers outfit', 'sports jersey fashion']","['블록코어 스타일', '축구 유니폼 스트릿 패션', '사커 저지와 청바지 코디', '아디다스 삼바 스니커즈 매치', '스포츠 저지 패션']"
16
+ girlcore,걸코어,"['girlcore', '걸코어룩', '걸리시룩', '하이틴 스타일']","소녀스러운 감성을 극대화한 스타일. 핑크 컬러, 리본, 레이스, 프릴, 크롭탑, 미니 스커트 등을 활용하여 귀엽고 발랄한 무드를 연출.","['girlcore aesthetic', 'girly fashion style', 'pink ribbon and lace details', 'cute crop top and mini skirt', 'k-pop idol style outfit']","['걸코어 스타일', '소녀 감성 패션', '핑크 리본과 레이스 디테일', '귀여운 크롭탑과 미니 스커트', '하이틴 영화 스타일']"
17
+ y2k,Y2K,"['Y2K fashion', 'Y2K 스타일', '2000년대 패션', '세기말 패션']","2000년대 초반(Year 2000) 유행했던 스타일. 로우라이즈 팬츠, 크롭탑, 벨벳 트레이닝 수트, 카고 팬츠, 화려한 그래픽과 컬러가 특징.","['Y2K fashion aesthetic', 'early 2000s style', 'low-rise jeans and crop top', 'velour tracksuit outfit', 'colorful graphic t-shirt']","['Y2K 패션', '2000년대 초반 스타일', '로우라이즈 팬츠와 크롭탑 코디', '벨벳 트레이닝 수트', '화려한 그래픽 티셔츠']"
18
+ copenhagen_core,코펜하겐 코어,"['copenhagen core', '코펜하겐 스타일', '덴마크 패션', '스칸디나비아 스타일']","덴마크 코펜하겐 여성들의 편안하면서도 컬러풀하고 실용적인 스트릿 스타일. 비비드한 컬러의 니트, 와이드 데님, 편안한 스니커즈 등이 특징.","['copenhagen street style', 'scandinavian fashion', 'colorful knit vest and wide-leg denim', 'vibrant color patterns', 'practical layered look with sneakers']","['코펜하겐 스트릿 스타일', '���칸디나비아 패션', '컬러풀한 니트 조끼와 와이드 데님', '생동감 있는 컬러 패턴', '스니커즈를 매치한 실용적인 레이어드 룩']"
19
+ old_money_look,올드머니룩,"['old money aesthetic', 'quiet luxury', '올드머니', '조용한 럭셔리']","과시하지 않는 상류층의 클래식하고 고급스러운 스타일. 로고 플레이를 배제하고, 린넨, 캐시미어 등 고급 소재와 뉴트럴 톤(베이지, 화이트, 네이비)을 주로 사용.","['quiet luxury aesthetic', 'old money style', 'linen shirt and beige slacks', 'minimalist luxury fashion without logos', 'classic preppy style with cashmere sweater']","['조용한 럭셔리 패션', '로고 없는 고급스러운 스타일', '린넨 셔츠와 베이지색 슬랙스 코디', '캐시미어 스웨터를 활용한 클래식 룩', '뉴트럴 톤의 우아한 아웃핏']"
20
+ running_core,러닝코어,"['running core', '러닝코어룩', '러닝화 코디', '스포츠웨어 믹스']","러닝(달리기)을 위한 기능성 의류를 일상복과 결합한 스타일. 고프코어와 유사하나, 러닝화, 바람막이, 레깅스, 스포츠 쇼츠 등 '러닝'에 더 집중.","['running core aesthetic', 'fashion with running shoes', 'windbreaker jacket and shorts', 'sportswear as daily outfit', 'technical running apparel']","['러닝코어 스타일', '러닝화 패션 코디', '바람막이 자켓과 쇼츠', '스포츠웨어 데일리룩', '기능성 러닝 의류']"
21
+ rock_chic,락시크,"['rock chic', '락시크룩', '락스타일', '글램락']","락(Rock) 음악 문화에서 영감을 받은 시크한 스타일. 가죽 재킷, 스키니진(주로 블랙), 밴드 티셔츠, 워커 부츠가 핵심 아이템.","['rock chic style', 'leather jacket and skinny jeans', 'band t-shirt with biker jacket', 'dark aesthetic with boots', 'glam rock fashion']","['락시크 스타일', '가죽 자켓과 스키니진', '밴드 티셔츠와 바이커 자켓', '부츠를 활용한 다크한 패션', '글램락 무드']"
22
+ grunge,그런지룩,"['grunge', '그런지', '너바나 스타일', 'nirvana style']","1990년대 락 밴드 '너바나'에서 유래한, 지저분하고 낡아 보이는 스타일. 찢어진 데님, 체크무늬 플란넬 셔츠, 빈티지 티셔츠, 워커 등을 레이어드.","['90s grunge aesthetic', 'kurt cobain style fashion', 'ripped jeans with flannel check shirt', 'vintage band t-shirt and worn-out boots', 'layered messy look']","['90년대 그런지 스타일', '커트 코베인 패션', '찢어진 청바지와 체크 플란넬 셔츠', '빈티지 밴드 티셔츠와 낡은 부츠', '지저분하게 레이어드한 룩']"
23
+ punk,펑크룩,"['punk', '펑크 스타일', '펑크 패션']","1970년대 펑크 락 문화에 기반한 반항적인 스타일. 스터드(징) 박힌 가죽 재킷, 찢어진 옷, 타탄 체크(플래드) 팬츠, 안전핀, 닥터마틴 워커가 상징적.","['punk fashion aesthetic', 'studded leather jacket', 'tartan plaid pants', 'ripped t-shirt with safety pins', 'dr. martens boots style']","['펑크 패션 스타일', '스터드 가죽 자켓', '타탄 체크 바지', '안전핀으로 고정한 찢어진 티셔츠', '닥터마틴 워커 코디']"
24
+ boho_chic,보호 시크,"['boho chic', '보호 시크룩', '페스티벌룩']","보헤미안(Bohemian) 스타일을 현대적이고 시크하게 재해석한 룩. 주로 페스티벌 룩으로 인기. 프린지(술 장식), 스웨이드, 로브 가디건, 에스닉 패턴이 특징.","['boho chic style', 'modern bohemian fashion', 'music festival outfit', 'suede jacket with fringe', 'ethnic pattern dress']","['보호 시크 스타일', '현대적인 보헤미안 패션', '뮤직 페스티벌 코디', '프린지 디테일의 스웨이드 자켓', '에스닉 패턴 원피스']"
25
+ bohemian,보헤미안,"['bohemian style', '보헤미안룩', '집시 스타일']","사회 관습에 얽매이지 않는 집시(Gypsy)들의 자유로운 스타일. 에스닉하고 이국적인 패턴, 루즈핏의 맥시 드레스, 자수, 천연 소재가 특징.","['bohemian aesthetic', 'gypsy style fashion', 'ethnic print maxi dress', 'loose fit natural fabric', 'embroidered tunic']","['보헤미안 스타일', '집시 패션', '에스닉 프린트 맥시 원피스', '루즈핏 천연 소재 의류', '자수가 놓인 튜닉']"
26
+ hippie_chic,히피 시크,"['hippie chic', '히피룩', '70년대 스타일']","1970년대 '히피' 문화에서 영감을 받은 스타일. 플라워 패턴, 플레어 팬츠(나팔바지), 크로셰(코바늘 뜨개) 니트, 헤어밴드, 평화의 상징 등이 특징.","['70s hippie style', 'hippie chic fashion', 'flare pants and flower pattern shirt', 'crochet knit vest', 'peace sign accessories']","['70년대 히피 스타일', '히피 시크 패션', '나팔바지와 플라워 패턴 셔츠', '크로셰 니트 조끼', '평화 상징 액세서리']"
27
+ new_romanticism,뉴로맨티시즘,"['new romanticism', '뉴로맨틱룩', '80년대 패션']","1980년대 초 영국 팝 문화(듀란듀란 등)에서 유래한 화려하고 젠더리스한 스타일. 프릴이나 러플이 달린 블라우스, 화려한 메이크업, 과장된 실루엣이 특징.","['80s new romantic fashion', 'duran duran style', 'frilly blouse with ruffles', 'flamboyant genderless look', 'dandy pirate aesthetic']","['80년대 뉴로맨틱 패션', '듀란듀란 스타일', '프릴과 러플이 달린 블라우스', '화려한 젠더리스 룩', '과장된 실루엣']"
28
+ mori_girl,모리걸,"['mori girl', '모리걸룩', '숲속의 소녀']","일본에서 유래한 '숲속의 소녀'를 뜻하는 스타일. 천연 소재(린넨, 코튼), 아스톤(Earth-tone) 컬러, 레이스, 자수 등을 활용해 여러 겹 레이어드하는 것이 특징.","['japanese mori girl style', 'forest girl aesthetic', 'layered natural fabrics dress', 'earth tone clothing with lace', 'vintage inspired loose fit']","['일본 모리걸 스타일', '숲속의 소녀 패션', '천연 소재 레이어드 원피스', '아스톤과 레이스 디테일', '빈티지 감성 루즈핏']"
29
+ natural,내추럴,"['natural style', '내추럴룩', '편안한 스타일', '린넨 코디']","자연스럽고 편안한 무드를 추구하는 스타일. 린넨, 코튼 등 천연 소재와 베이지, 아이보리, 브라운 등 뉴트럴 컬러, 루즈핏 실루엣이 중심.","['natural aesthetic fashion', 'comfortable loose fit', 'linen shirt and cotton pants', 'neutral color palette', 'simple effortless look']","['내추럴 스타일 패션', '편안한 루즈핏', '린넨 셔츠와 코튼 팬츠', '뉴트럴 컬러 코디', '꾸밈없는 편안한 룩']"
30
+ french_chic,프렌치시크,"['french chic', '프렌치시크룩', '파리지앵 스타일']","프랑스 파리 여성들의 무심한 듯 시크한 스타일. 스트라이프 티셔츠(브레통 셔츠), 트렌치 코트, 발레리나 플랫, 레드 립 등이 상징적.","['french chic aesthetic', 'parisian style fashion', 'breton stripe shirt', 'effortless chic with trench coat', 'jeanne damas style']","['프렌치시크 스타일', '파리지앵 패션', '스트라이프 티셔츠 (브레통 셔츠)', '트렌치 코트를 활용한 무심한 시크함', '제인 버킨 스타일']"
31
+ western_look,웨스턴룩,"['western look', '웨스턴 스타일', '카우보이 스타일']","미국 서부 카우보이 스타일에서 영감을 받은 룩. 데님(청청 패션), 웨스턴 부츠, 프린지 장식, 웨스턴 셔츠, 볼로 타이 등이 특징.","['western fashion style', 'cowboy aesthetic', 'denim on denim with cowboy boots', 'western shirt with fringe jacket', 'bolo tie accessory']","['웨스턴 패션 스타일', '카우보이 스타일', '청청 패션과 웨스턴 부츠', '웨스턴 셔츠와 프린지 자켓', '볼로 타이 액세서리']"
32
+ minimal,미니멀,"['minimalism', '미니멀리즘', '미니멀룩']","장식적인 요소를 최소화하고 단순함을 추구하는 스타일. 깔끔한 실루엣, 무채색(블랙, 화이트, 그레이) 및 뉴트럴 톤, 로고가 없는 디자인이 특징.","['minimalist fashion', 'clean silhouette outfit', 'monotone clothing', 'simple and modern look', 'unembellished design']","['미니멀 패션', '깔끔한 실루엣의 아웃핏', '무채색 코디', '심플하고 모던한 룩', '장식 없는 디자인']"
33
+ simple_basic,심플 베이직,"['simple basic', '심플 베이직룩', '기본템 코디']","미니멀과 유사하나, '기본 아이템'에 더 초점을 맞춘 스타일. 흰 티셔츠, 기본 셔츠, 청바지, 슬랙스 등 유행을 타지 않는 기본 아이템 위주의 조합.","['simple basic style', 'wardrobe essentials', 'white t-shirt and blue jeans', 'classic button-down shirt', 'timeless basic items']","['심플 베이직 스타일', '기본 아이템 코디', '흰 티셔츠와 청바지', '클래식 버튼다운 셔츠', '유행 안 타는 기본템']"
34
+ modern_chic,모던시크,"['modern chic', '모던시크룩', '도시적인 스타일']","현대적이고(Modern) 세련된(Chic) 도시적인 무드의 스타일. 미니멀을 기반으로 하되, 구조적인 실루엣(블레이저, 와이드 슬랙스)이나 포인트 컬러를 사용해 세련미를 강조.","['modern chic aesthetic', 'urban sophisticated style', 'structured blazer outfit', 'wide-leg slacks', 'minimal but sharp look']","['모던시크 스타일', '도시적이고 세련된 룩', '각 잡힌 블레이저 코디', '와이드 슬랙스', '미니멀하지만 날카로운 룩']"
35
+ modern_classic,모던 클래식,"['modern classic', '모던 클래식룩']","전통적인 클래식 아이템(트렌치 코트, 테일러드 수트, 로퍼)을 현대적인 실루엣이나 소재와 결합하여 재해석한 스타일.","['modern classic style', 'timeless fashion with modern twist', 'classic trench coat', 'tailored suit', 'loafers with slacks']","['모던 클래식 스타일', '현대적으로 재해석한 클래식 패션', '클래식 트렌치 코트', '테일러드 수트', '로퍼와 슬랙스 코디']"
36
+ dandy,댄디,"['dandy style', '댄디룩', '깔끔한 남성복']","멋을 내는 남성(Dandy)이라는 뜻에서 유래한, 깔끔하고 클래식한 남성 스타일. 주로 테일러드 재킷, 셔츠, 슬랙스, 니트, 로퍼 등을 활용.","['dandy style for men', ""clean and neat men's fashion"", 'tailored jacket with slacks', 'knitwear over shirt', 'classic loafers']","['댄디 스타일', '깔끔한 남자 패션', '테일러드 자켓과 슬랙스', '셔츠 위 니트 레이어드', '클래식 로퍼 코디']"
37
+ vintage,빈티지,"['vintage style', '빈티지룩', '구제 패션', '레트로']","과거 특정 시대(주로 20세기)의 패션을 재현하거나 실제 중고(구제) 의류를 활용하는 스타일. 바랜 색감, 레트로 그래픽, 고유한 패턴이 특징.","['vintage fashion aesthetic', 'retro style clothing', 'secondhand clothing look', '80s or 90s inspired outfit', 'faded colors and retro graphics']","['빈티지 패션 스타일', '레트로 스타일 의류', '구제 옷 코디', '80년대 또는 90년대 감성 룩', '빛 바랜 색감과 레트로 그래픽']"
38
+ street_fashion,스트릿 패션,"['streetwear', '스트릿룩', '스트리트 패션']","(대분류) 스케이트보드, 힙합, 서핑 등 도시의 길거리(Street) 하위문화에서 발생한 패션. 루즈핏, 그래픽 티셔츠, 후드티, 스니커즈가 중심.","['streetwear fashion', 'urban street style', 'graphic hoodie and baggy jeans', 'sneakers culture', 'supreme style']","['스트릿웨어 패션', '도시 스트릿 스타일', '그래픽 후드와 배기진', '스니커즈 문화', '슈프림 스타일']"
39
+ hip_hop,힙합,"['hip hop fashion', '힙합 스타일', '스웩']","힙합 문화에서 파생된 스타일. 매우 넉넉한 오버사이징(배기 팬츠, 큰 후드), 스냅백, 볼드한 액세서리(체인), 농구화 등이 특징.","['hip hop fashion style', '90s hip hop look', 'oversized hoodie and baggy pants', 'snapback cap and gold chain', 'basketball sneakers']","['힙합 패션 스타일', '90년대 힙합 룩', '오버사이즈 후드와 배기 팬츠', '스냅백과 골드 체인', '농구화 코디']"
40
+ skater_look,스케이터룩,"['skater style', '스케이터 스타일', '보드룩']","스케이트보더들의 스타일. 활동성을 위한 루즈핏 티셔츠, 반스(Vans)나 컨버스(Converse) 같은 스케이트 슈즈, 디키즈(Dickies) 팬츠, 볼캡이 상징적.","['skater style aesthetic', 'skateboarder fashion', 'loose fit graphic t-shirt', 'vans old skool sneakers', 'dickies pants and beanie']","['스케이터 스타일', '스케이트보더 패션', '루즈핏 그래픽 티셔츠', '반스 올드스쿨 스니커즈', '디키즈 팬츠와 비니']"
41
+ techwear,테크웨어,"['techwear', '테크웨어룩', '아크로님 스타일']","고프코어와 유사하나, 더 도시적이고 전술적(Tactical)인 무드를 가짐. 기능성 방수/방풍 소재, 많은 포켓, 스트랩, 버클 디테일, 어두운 컬러가 특징.","['techwear aesthetic', 'urban functional fashion', 'jacket with many pockets and straps', 'waterproof cargo pants', 'acronym style']","['테크웨어 스타일', '도시형 기능성 패션', '포켓과 스트랩이 많은 자켓', '방수 카고 팬츠', '아크로님 스타일']"
42
+ workwear,워크웨어,"['workwear', '워크웨어룩', '아메카지 기반']","19-20세기 미국 노동자들의 작업복에서 유래한 스타일. 튼튼한 데님(진), 덕 캔버스 소재(칼하트), 워커 부츠, 오버롤(멜빵바지) 등이 특징.","['workwear style', 'american casual fashion', 'carhartt duck jacket', 'denim overalls', 'timberland boots']","['워크웨어 스타일', '아메리칸 캐주얼 패션', '칼하트 덕 자켓', '데님 오버롤 (멜빵바지)', '팀버랜드 워커']"
43
+ military_look,밀리터리룩,"['military look', '밀리터리 스타일', '군복 패션']","군복(Military uniform)에서 영감을 받은 스타일. 카모플라주(위장) 패턴, 카고 팬츠(건빵바지), 야상 점퍼(M-65), 항공 점퍼(MA-1)가 대표 아이템.","['military fashion style', 'camouflage pattern', 'cargo pants', 'M-65 field jacket', 'MA-1 bomber jacket']","['밀리터리 패션 스타일', '카모플라주 패턴 (군복 무늬)', '카고 팬츠 (건빵바지)', '야상 점퍼', '항공 점퍼']"
44
+ city_boy,시티보이룩,"['city boy', '시티보이', '씨티보이', '뽀빠이 매거진 스타일']","일본 '뽀빠이(Popeye)' 매거진에서 유래한 스타일. 넉넉하고 여유 있는 실루엣이 특징. 오버핏 셔츠, 치노 팬츠, 볼캡, 스니커즈 등을 주로 매치.","['japanese city boy aesthetic', 'oversized button-down shirt with wide chino pants', 'skater style mixed with preppy look', 'loose fit casual outfit with baseball cap', ""style from 'popeye' magazine""]","['일본 시티보이 스타일', '오버사이즈 셔츠와 와이드 치노 팬츠', '스케이터와 프레피가 섞인 룩', '볼캡을 매치한 루즈핏 코디', '뽀빠이 매거진 스타일']"
45
+ kku_an_kku,꾸안꾸,"['꾸안꾸룩', 'effortless chic', 'korean natural style']","'꾸민 듯 안 꾸민 듯'의 줄임말. 편안해 보이지만(스웻셔츠, 루즈핏 니트) 실루엣이나 소재, 액세서리 등으로 은근히 멋을 낸 자연스러운 스타일.","[""korean 'kku-an-kku' style"", 'effortless chic', 'natural and simple but stylish', 'oversized sweater with simple accessories', 'casual but fashionable']","['꾸안꾸 스타일', '무심한 듯 시크한 룩', '자연스럽지만 멋스러운 코디', '오버사이즈 스웨터와 심플한 액세서리', '편안하지만 패셔너블한']"
46
+ campus_look,캠퍼스룩,"['campus look', '대학생 코디', '프레피 캐주얼']","대학생들의 편안하면서도 단정한 스타일. 후드티, 맨투맨, 바시티 재킷(과잠), 백팩, 스니커즈, 청바지 등이 주요 아이템.","['campus look aesthetic', 'college student style', 'varsity jacket with hoodie', 'backpack and sneakers outfit', 'preppy casual look']","['캠퍼스룩 스타일', '대학생 코디', '바시티 자켓(과잠)과 후드티', '백팩과 스니커즈 코디', '프레피 캐주얼 룩']"
47
+ one_mile_wear,원마일웨어,"['one-mile wear', '원마일웨어룩', '라운지웨어', '집 근처 패션']","집 근처 1마일(약 1.6km) 반경 내에서 입기 좋은 편안하고 실용적인 옷. 주로 스웻 셋업(트레이닝복), 후드 집업, 조거 팬츠, 레깅스 등.","['one-mile wear', 'loungewear', 'comfortable sweat suit set', 'jogger pants and hoodie zip-up', 'athleisure look']","['원마일웨어', '라운지웨어', '편안한 스웻 셋업 (트레이닝복 세트)', '조거 팬츠와 후드 집업', '애슬레저 룩']"
48
+ mix_match,믹스매치,"['mix and match', '믹스매치룩', '스타일 믹스']","서로 다른 스타일, 소재, 패턴의 아이템을 의도적으로 혼합하여 새로운 조화를 이끌어내는 코디 방식. (예: 포멀한 재킷 + 캐주얼한 조거 팬츠)","['mix and match fashion style', 'mixing different styles', 'formal jacket with casual pants', 'pattern mixing outfit', 'unexpected combination']","['믹스매치 패션 스타일', '서로 다른 스타일 조합', '포멀한 자켓과 캐주얼한 바지', '패턴 믹스 코디', '의외의 조합']"
49
+ sports_mix,스포츠믹스,"['sports mix', '스포츠믹스룩', '애슬레저']","믹스매치의 일종으로, 스포츠웨어(트랙 팬츠, 저지, 아노락)를 일상복(재킷, 셔츠, 코트)과 조합하는 스타일.","['sports mix style', 'athleisure fashion', 'track pants with formal jacket', 'sports jersey with coat', 'sporty and casual mix']","['스포츠믹스 스타일', '애슬레저 패션', '트랙 팬츠(추리닝)와 포멀한 자켓', '스포츠 저지와 코트 조합', '스포티 캐주얼 믹스']"