11package com .DecodEat .domain .products .service ;
22
3+ import com .DecodEat .domain .products .client .PythonAnalysisClient ;
34import com .DecodEat .domain .products .converter .ProductConverter ;
5+ import com .DecodEat .domain .products .dto .request .AnalysisRequestDto ;
46import com .DecodEat .domain .products .dto .request .ProductRegisterRequestDto ;
57import com .DecodEat .domain .products .dto .response .*;
68import com .DecodEat .domain .products .entity .DecodeStatus ;
79import com .DecodEat .domain .products .entity .Product ;
810import com .DecodEat .domain .products .entity .ProductInfoImage ;
911import com .DecodEat .domain .products .entity .ProductNutrition ;
12+ import com .DecodEat .domain .products .entity .ProductRawMaterial ;
13+ import com .DecodEat .domain .products .entity .RawMaterial .RawMaterial ;
1014import com .DecodEat .domain .products .entity .RawMaterial .RawMaterialCategory ;
1115import com .DecodEat .domain .products .repository .ProductImageRepository ;
1216import com .DecodEat .domain .products .repository .ProductNutritionRepository ;
17+ import com .DecodEat .domain .products .repository .ProductRawMaterialRepository ;
1318import com .DecodEat .domain .products .repository .ProductRepository ;
19+ import com .DecodEat .domain .products .repository .RawMaterialRepository ;
1420import com .DecodEat .domain .products .repository .ProductSpecification ;
1521import com .DecodEat .domain .users .entity .User ;
1622import com .DecodEat .global .aws .s3 .AmazonS3Manager ;
1723import com .DecodEat .global .dto .PageResponseDto ;
1824import com .DecodEat .global .exception .GeneralException ;
1925import lombok .RequiredArgsConstructor ;
26+ import lombok .extern .slf4j .Slf4j ;
2027import org .springframework .data .domain .*;
2128import org .springframework .data .jpa .domain .Specification ;
29+ import org .springframework .scheduling .annotation .Async ;
2230import org .springframework .stereotype .Service ;
2331import org .springframework .transaction .annotation .Transactional ;
2432import org .springframework .util .StringUtils ;
3543@ Service
3644@ RequiredArgsConstructor
3745@ Transactional
46+ @ Slf4j
3847public class ProductService {
3948 private final ProductRepository productRepository ;
4049 private final ProductImageRepository productImageRepository ;
4150 private final ProductNutritionRepository productNutritionRepository ;
51+ private final RawMaterialRepository rawMaterialRepository ;
52+ private final ProductRawMaterialRepository productRawMaterialRepository ;
4253 private final AmazonS3Manager amazonS3Manager ;
54+ private final PythonAnalysisClient pythonAnalysisClient ;
4355
4456
4557 private static final int PAGE_SIZE = 12 ;
@@ -91,6 +103,9 @@ public ProductRegisterResponseDto addProduct(User user, ProductRegisterRequestDt
91103 productInfoImageUrls = infoImages .stream ().map (ProductInfoImage ::getImageUrl ).toList ();
92104 }
93105
106+ // 파이썬 서버에 비동기로 분석 요청
107+ requestAnalysisAsync (savedProduct .getProductId (), productInfoImageUrls );
108+
94109 return ProductConverter .toProductRegisterDto (savedProduct , productInfoImageUrls );
95110 }
96111
@@ -152,4 +167,154 @@ public PageResponseDto<ProductRegisterHistoryDto> getRegisterHistory(User user,
152167
153168 return new PageResponseDto <>(result );
154169 }
170+
171+ @ Async
172+ public void requestAnalysisAsync (Long productId , List <String > imageUrls ) {
173+ log .info ("Starting async analysis for product ID: {}" , productId );
174+
175+ if (imageUrls == null || imageUrls .isEmpty ()) {
176+ log .warn ("No images to analyze for product ID: {}" , productId );
177+ updateProductStatus (productId , DecodeStatus .FAILED , "No images provided for analysis" );
178+ return ;
179+ }
180+
181+ try {
182+ AnalysisRequestDto request = AnalysisRequestDto .builder ()
183+ .image_urls (imageUrls )
184+ .build ();
185+
186+ pythonAnalysisClient .analyzeProduct (request )
187+ .subscribe (
188+ response -> processAnalysisResult (productId , response ),
189+ error -> {
190+ log .error ("Analysis failed for product ID: {}" , productId , error );
191+ updateProductStatus (productId , DecodeStatus .FAILED , "Analysis request failed: " + error .getMessage ());
192+ }
193+ );
194+ } catch (Exception e ) {
195+ log .error ("Failed to send analysis request for product ID: {}" , productId , e );
196+ updateProductStatus (productId , DecodeStatus .FAILED , "Failed to send analysis request" );
197+ }
198+ }
199+
200+ @ Transactional
201+ public void processAnalysisResult (Long productId , AnalysisResponseDto response ) {
202+ log .info ("Processing analysis result for product ID: {} with status: {}" , productId , response .getDecodeStatus ());
203+
204+ try {
205+ Product product = productRepository .findById (productId )
206+ .orElseThrow (() -> new GeneralException (PRODUCT_NOT_EXISTED ));
207+
208+ // 상품 상태 업데이트
209+ product .setDecodeStatus (response .getDecodeStatus ());
210+ productRepository .save (product );
211+
212+ // 분석이 성공한 경우 영양정보 저장
213+ if (response .getDecodeStatus () == DecodeStatus .COMPLETED && response .getNutrition_info () != null ) {
214+ saveNutritionInfo (productId , response );
215+ }
216+
217+ log .info ("Successfully processed analysis result for product ID: {}" , productId );
218+ } catch (Exception e ) {
219+ log .error ("Failed to process analysis result for product ID: {}" , productId , e );
220+ updateProductStatus (productId , DecodeStatus .FAILED , "Failed to process analysis result" );
221+ }
222+ }
223+
224+ @ Transactional
225+ public void updateProductStatus (Long productId , DecodeStatus status , String message ) {
226+ try {
227+ Product product = productRepository .findById (productId )
228+ .orElseThrow (() -> new GeneralException (PRODUCT_NOT_EXISTED ));
229+
230+ product .setDecodeStatus (status );
231+ productRepository .save (product );
232+
233+ log .info ("Updated product ID: {} status to: {} - {}" , productId , status , message );
234+ } catch (Exception e ) {
235+ log .error ("Failed to update product status for ID: {}" , productId , e );
236+ }
237+ }
238+
239+ private void saveNutritionInfo (Long productId , AnalysisResponseDto response ) {
240+ log .info ("Saving nutrition info for product ID: {}" , productId );
241+
242+ try {
243+ Product product = productRepository .findById (productId )
244+ .orElseThrow (() -> new GeneralException (PRODUCT_NOT_EXISTED ));
245+
246+ // 영양정보 저장
247+ if (response .getNutrition_info () != null ) {
248+ ProductNutrition nutrition = ProductNutrition .builder ()
249+ .product (product )
250+ .calcium (parseDouble (response .getNutrition_info ().getCalcium ()))
251+ .carbohydrate (parseDouble (response .getNutrition_info ().getCarbohydrate ()))
252+ .cholesterol (parseDouble (response .getNutrition_info ().getCholesterol ()))
253+ .dietaryFiber (parseDouble (response .getNutrition_info ().getDietary_fiber ()))
254+ .energy (parseDouble (response .getNutrition_info ().getEnergy ()))
255+ .fat (parseDouble (response .getNutrition_info ().getFat ()))
256+ .protein (parseDouble (response .getNutrition_info ().getProtein ()))
257+ .satFat (parseDouble (response .getNutrition_info ().getSat_fat ()))
258+ .sodium (parseDouble (response .getNutrition_info ().getSodium ()))
259+ .sugar (parseDouble (response .getNutrition_info ().getSugar ()))
260+ .transFat (parseDouble (response .getNutrition_info ().getTrans_fat ()))
261+ .build ();
262+
263+ productNutritionRepository .save (nutrition );
264+ log .info ("Saved nutrition info for product ID: {}" , productId );
265+ }
266+
267+ // 원재료 정보 저장
268+ if (response .getIngredients () != null && !response .getIngredients ().isEmpty ()) {
269+ saveIngredients (product , response .getIngredients ());
270+ log .info ("Saved {} ingredients for product ID: {}" , response .getIngredients ().size (), productId );
271+ }
272+
273+ } catch (Exception e ) {
274+ log .error ("Failed to save nutrition info for product ID: {}" , productId , e );
275+ throw e ;
276+ }
277+ }
278+
279+ private void saveIngredients (Product product , List <String > ingredientNames ) {
280+ // 기존 원재료 관계 삭제
281+ productRawMaterialRepository .deleteByProduct (product );
282+
283+ for (String ingredientName : ingredientNames ) {
284+ if (ingredientName != null && !ingredientName .trim ().isEmpty ()) {
285+ // 원재료가 이미 존재하는지 확인
286+ RawMaterial rawMaterial = rawMaterialRepository .findByName (ingredientName .trim ())
287+ .orElseGet (() -> {
288+ // 새로운 원재료 생성 (기본 카테고리는 OTHERS)
289+ RawMaterial newRawMaterial = RawMaterial .builder ()
290+ .name (ingredientName .trim ())
291+ .category (RawMaterialCategory .OTHERS )
292+ .build ();
293+ return rawMaterialRepository .save (newRawMaterial );
294+ });
295+
296+ // 상품-원재료 관계 생성
297+ ProductRawMaterial productRawMaterial = ProductRawMaterial .builder ()
298+ .product (product )
299+ .rawMaterial (rawMaterial )
300+ .build ();
301+
302+ productRawMaterialRepository .save (productRawMaterial );
303+ }
304+ }
305+ }
306+
307+ private Double parseDouble (String value ) {
308+ if (value == null || value .trim ().isEmpty ()) {
309+ return null ;
310+ }
311+ try {
312+ // 숫자가 아닌 문자 제거 (단위 등)
313+ String cleanValue = value .replaceAll ("[^0-9.]" , "" );
314+ return cleanValue .isEmpty () ? null : Double .parseDouble (cleanValue );
315+ } catch (NumberFormatException e ) {
316+ log .warn ("Failed to parse double value: {}" , value );
317+ return null ;
318+ }
319+ }
155320}
0 commit comments