From 6139b20daf079a4f7ae45facaccab239566fa012 Mon Sep 17 00:00:00 2001 From: eunhye Date: Thu, 22 Jan 2026 11:41:06 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20React=20Query=20+=20Zustand=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=20=EB=B0=8F=20=EB=B6=84=EC=9F=81=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 백엔드: Boonjang 엔티티, Repository, Service, Controller 추가 - 프론트엔드: React Query 훅, Zustand 인증 스토어, API 클라이언트 추가 - UI: 새 헤더/푸터 디자인 적용, boonjang 페이지 리팩토링 --- .../controller/BoonjangApiController.java | 83 ++++++ .../com/oracle/Legal/domain/Boonjang.java | 63 +++++ .../com/oracle/Legal/dto/BoonjangDto.java | 76 ++++++ .../Legal/repository/BoonjangRepository.java | 39 +++ .../oracle/Legal/service/BoonjangService.java | 178 +++++++++++++ .../my-react-app/component/footer.jsx | 62 +++++ .../my-react-app/component/header.jsx | 70 +++++ legal/FrontEnd/my-react-app/src/App.jsx | 53 +++- legal/FrontEnd/my-react-app/src/api/client.js | 70 +++++ .../FrontEnd/my-react-app/src/api/queries.js | 126 +++++++++ legal/FrontEnd/my-react-app/src/main.jsx | 33 ++- .../my-react-app/src/pages/boonjang.jsx | 240 +++++++++++++++--- .../my-react-app/src/stores/authStore.js | 84 ++++++ 13 files changed, 1125 insertions(+), 52 deletions(-) create mode 100644 legal/BackEnd/src/main/java/com/oracle/Legal/controller/BoonjangApiController.java create mode 100644 legal/BackEnd/src/main/java/com/oracle/Legal/domain/Boonjang.java create mode 100644 legal/BackEnd/src/main/java/com/oracle/Legal/dto/BoonjangDto.java create mode 100644 legal/BackEnd/src/main/java/com/oracle/Legal/repository/BoonjangRepository.java create mode 100644 legal/BackEnd/src/main/java/com/oracle/Legal/service/BoonjangService.java create mode 100644 legal/FrontEnd/my-react-app/src/api/client.js create mode 100644 legal/FrontEnd/my-react-app/src/api/queries.js create mode 100644 legal/FrontEnd/my-react-app/src/stores/authStore.js diff --git a/legal/BackEnd/src/main/java/com/oracle/Legal/controller/BoonjangApiController.java b/legal/BackEnd/src/main/java/com/oracle/Legal/controller/BoonjangApiController.java new file mode 100644 index 0000000..cd69e48 --- /dev/null +++ b/legal/BackEnd/src/main/java/com/oracle/Legal/controller/BoonjangApiController.java @@ -0,0 +1,83 @@ +package com.oracle.Legal.controller; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.oracle.Legal.dto.BoonjangDto; +import com.oracle.Legal.service.BoonjangService; + +import lombok.RequiredArgsConstructor; + +/** + * 분쟁 유형 분류 API 컨트롤러 + * + * [역할] + * - React 프론트엔드에서 호출하는 REST API 엔드포인트 제공 + * - /api/boonjang 경로로 분쟁 데이터 저장 및 조회 + * + * [엔드포인트] + * - POST /api/boonjang : 분쟁 내용 저장 및 분석 + * - GET /api/boonjang : 분쟁 목록 조회 + * + * [데이터 흐름] + * React → POST /api/boonjang → Controller → Service → Repository → DB + * ↓ + * 분석 결과 반환 + */ +@org.springframework.web.bind.annotation.RestController +@RequestMapping("/api/boonjang") +@RequiredArgsConstructor +public class BoonjangApiController { + + private final BoonjangService boonjangService; + + /** + * 분쟁 내용 분석 및 저장 + * + * [요청 형식] + * POST /api/boonjang + * Content-Type: application/json + * { + * "boonjangInput": "분쟁 내용...", + * "clientCode": 1001 (선택, 로그인한 사용자) + * } + * + * [응답 형식] + * { + * "boonjangId": 1001, + * "classification": "민사", + * "subType": "소비자", + * "summary": "...", + * "keywords": ["#청약철회", ...], + * "judgment": "...", + * "relatedLaws": ["전자상거래법", ...] + * } + */ + @PostMapping + public ResponseEntity analyzeBoonjang( + @RequestBody BoonjangDto.Request request) { + + BoonjangDto.Response result = boonjangService.analyze(request); + return ResponseEntity.ok(result); + } + + /** + * 분쟁 목록 조회 + * + * [응답 형식] + * [ + * { "boonjangId": 1001, "boonjangInput": "...", "boonjangDate": "..." }, + * ... + * ] + */ + @GetMapping + public ResponseEntity> getBoonjangList() { + List list = boonjangService.findAll(); + return ResponseEntity.ok(list); + } +} diff --git a/legal/BackEnd/src/main/java/com/oracle/Legal/domain/Boonjang.java b/legal/BackEnd/src/main/java/com/oracle/Legal/domain/Boonjang.java new file mode 100644 index 0000000..820d53f --- /dev/null +++ b/legal/BackEnd/src/main/java/com/oracle/Legal/domain/Boonjang.java @@ -0,0 +1,63 @@ +package com.oracle.Legal.domain; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 분쟁 엔티티 + * + * [DB 테이블: BOONJANG] + * - BOONJANG_ID: 분쟁 ID (기본키, 시퀀스) + * - CLIENT_CODE: 회원 코드 (외래키) + * - BOONJANG_DATE: 등록 일시 + * - BOONJANG_INPUT: 분쟁 내용 입력 + * - BOONJANG_OUTPUT: 분석 결과 출력 + * + * [원리] + * @Entity: JPA가 이 클래스를 DB 테이블과 매핑 + * @Table: 실제 테이블 이름 지정 + * @Id + @GeneratedValue: 기본키 자동 생성 전략 + * @SequenceGenerator: Oracle 시퀀스 사용 + */ +@Entity +@Table(name = "BOONJANG") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@SequenceGenerator( + name = "boonjang_seq_gen", + sequenceName = "BOONJANG_SEQ", + initialValue = 1, + allocationSize = 1 +) +public class Boonjang { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "boonjang_seq_gen") + @Column(name = "BOONJANG_ID") + private Long boonjangId; + + @Column(name = "CLIENT_CODE") + private Long clientCode; + + @Column(name = "BOONJANG_DATE") + private LocalDateTime boonjangDate; + + @Column(name = "BOONJANG_INPUT", length = 255) + private String boonjangInput; + + @Column(name = "BOONJANG_OUTPUT", length = 255) + private String boonjangOutput; +} diff --git a/legal/BackEnd/src/main/java/com/oracle/Legal/dto/BoonjangDto.java b/legal/BackEnd/src/main/java/com/oracle/Legal/dto/BoonjangDto.java new file mode 100644 index 0000000..b032d80 --- /dev/null +++ b/legal/BackEnd/src/main/java/com/oracle/Legal/dto/BoonjangDto.java @@ -0,0 +1,76 @@ +package com.oracle.Legal.dto; + +import java.time.LocalDateTime; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 분쟁 DTO (Data Transfer Object) + * + * [원리] + * DTO는 계층 간 데이터 전송에 사용됩니다. + * + * Entity vs DTO: + * - Entity: DB 테이블과 1:1 매핑, JPA가 관리 + * - DTO: API 요청/응답용, 필요한 필드만 포함 + * + * 왜 분리하는가? + * 1. Entity에 민감한 필드가 있을 수 있음 (비밀번호 등) + * 2. API 응답에 Entity에 없는 추가 정보 포함 가능 + * 3. Entity 변경이 API에 영향 주지 않음 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BoonjangDto { + + // 기본 정보 + private Long boonjangId; + private Long clientCode; + private LocalDateTime boonjangDate; + private String boonjangInput; + private String boonjangOutput; + + // 분석 결과 (Entity에는 없지만 API 응답에 필요) + private String classification; // 분류 (민사, 형사 등) + private String subType; // 세부 유형 (소비자, 계약 등) + private String summary; // 요약 + private List keywords; // 키워드 태그 + private String judgment; // 법률적 판단 + private List relatedLaws; // 관련 법령 + + /** + * 요청 DTO (내부 클래스) + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Request { + private String boonjangInput; + private Long clientCode; // 로그인한 사용자 코드 (선택) + } + + /** + * 응답 DTO (내부 클래스) + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Response { + private Long boonjangId; + private String classification; + private String subType; + private String summary; + private List keywords; + private String judgment; + private List relatedLaws; + private String boonjangInput; + private LocalDateTime boonjangDate; + } +} diff --git a/legal/BackEnd/src/main/java/com/oracle/Legal/repository/BoonjangRepository.java b/legal/BackEnd/src/main/java/com/oracle/Legal/repository/BoonjangRepository.java new file mode 100644 index 0000000..cfe59bf --- /dev/null +++ b/legal/BackEnd/src/main/java/com/oracle/Legal/repository/BoonjangRepository.java @@ -0,0 +1,39 @@ +package com.oracle.Legal.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.oracle.Legal.domain.Boonjang; + +/** + * 분쟁 레포지토리 + * + * [원리] + * JpaRepository를 상속하면 기본 CRUD 메서드가 자동 제공됩니다: + * - save(entity): INSERT 또는 UPDATE + * - findById(id): SELECT by ID + * - findAll(): SELECT ALL + * - deleteById(id): DELETE by ID + * + * 추가 메서드는 메서드 이름 규칙으로 자동 생성: + * - findByClientCode(code) → SELECT * FROM BOONJANG WHERE CLIENT_CODE = ? + * - findByBoonjangDateDesc() → SELECT * ... ORDER BY BOONJANG_DATE DESC + */ +@Repository +public interface BoonjangRepository extends JpaRepository { + + /** + * 회원 코드로 분쟁 목록 조회 + * + * [JPA 쿼리 메서드 규칙] + * findBy + 필드명 → WHERE 조건 자동 생성 + */ + List findByClientCode(Long clientCode); + + /** + * 최신 순으로 전체 조회 + */ + List findAllByOrderByBoonjangDateDesc(); +} diff --git a/legal/BackEnd/src/main/java/com/oracle/Legal/service/BoonjangService.java b/legal/BackEnd/src/main/java/com/oracle/Legal/service/BoonjangService.java new file mode 100644 index 0000000..54b6025 --- /dev/null +++ b/legal/BackEnd/src/main/java/com/oracle/Legal/service/BoonjangService.java @@ -0,0 +1,178 @@ +package com.oracle.Legal.service; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.oracle.Legal.domain.Boonjang; +import com.oracle.Legal.dto.BoonjangDto; +import com.oracle.Legal.repository.BoonjangRepository; + +import lombok.RequiredArgsConstructor; + +/** + * 분쟁 서비스 + * + * [역할] + * - 비즈니스 로직 처리 + * - 트랜잭션 관리 + * - Entity ↔ DTO 변환 + * + * [흐름] + * Controller → Service → Repository → DB + * ↑ ↑ + * DTO Entity + * + * [원리] + * @Service: 스프링이 이 클래스를 서비스 빈으로 등록 + * @Transactional: 메서드 실행 시 트랜잭션 시작, 완료 시 커밋, 예외 시 롤백 + * @RequiredArgsConstructor: final 필드에 대한 생성자 자동 생성 (의존성 주입) + */ +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BoonjangService { + + private final BoonjangRepository boonjangRepository; + + /** + * 분쟁 내용 분석 및 저장 + * + * [처리 순서] + * 1. 입력 데이터 검증 + * 2. Entity 생성 및 저장 + * 3. 분석 수행 (현재는 임시 로직) + * 4. 결과 DTO 반환 + * + * @param request 분쟁 입력 요청 + * @return 분석 결과 DTO + */ + @Transactional + public BoonjangDto.Response analyze(BoonjangDto.Request request) { + + // 1. Entity 생성 + Boonjang boonjang = Boonjang.builder() + .clientCode(request.getClientCode() != null ? request.getClientCode() : 0L) + .boonjangDate(LocalDateTime.now()) + .boonjangInput(request.getBoonjangInput()) + .boonjangOutput(null) // 분석 후 업데이트 + .build(); + + // 2. DB 저장 + Boonjang saved = boonjangRepository.save(boonjang); + + // 3. 분석 수행 (TODO: 실제 AI 모델 연동) + AnalysisResult analysis = analyzeBoonjangContent(request.getBoonjangInput()); + + // 4. 분석 결과를 Entity에 저장 + saved.setBoonjangOutput(analysis.getSummary()); + boonjangRepository.save(saved); + + // 5. 응답 DTO 생성 + return BoonjangDto.Response.builder() + .boonjangId(saved.getBoonjangId()) + .classification(analysis.getClassification()) + .subType(analysis.getSubType()) + .summary(analysis.getSummary()) + .keywords(analysis.getKeywords()) + .judgment(analysis.getJudgment()) + .relatedLaws(analysis.getRelatedLaws()) + .boonjangInput(saved.getBoonjangInput()) + .boonjangDate(saved.getBoonjangDate()) + .build(); + } + + /** + * 분쟁 목록 조회 + */ + public List findAll() { + return boonjangRepository.findAllByOrderByBoonjangDateDesc() + .stream() + .map(this::toResponse) + .collect(Collectors.toList()); + } + + /** + * Entity → Response DTO 변환 + */ + private BoonjangDto.Response toResponse(Boonjang entity) { + return BoonjangDto.Response.builder() + .boonjangId(entity.getBoonjangId()) + .boonjangInput(entity.getBoonjangInput()) + .boonjangDate(entity.getBoonjangDate()) + .build(); + } + + /** + * 분쟁 내용 분석 (임시 로직) + * + * [TODO] + * - 실제 AI 모델 연동 + * - 또는 키워드 기반 규칙 적용 + */ + private AnalysisResult analyzeBoonjangContent(String input) { + // 임시 분석 로직 - 키워드 기반 + String classification = "민사"; + String subType = "소비자"; + List keywords = Arrays.asList("#청약철회", "#전자상거래", "#환불거부"); + List relatedLaws = Arrays.asList( + "전자상거래 등에서의 소비자보호에 관한 법률", + "소비자기본법" + ); + + // 키워드 기반 간단한 분류 + if (input.contains("계약") || input.contains("위약금")) { + subType = "계약"; + keywords = Arrays.asList("#계약위반", "#위약금", "#손해배상"); + relatedLaws = Arrays.asList("민법 제390조 (채무불이행)", "민법 제398조 (손해배상의 예정)"); + } + if (input.contains("임대") || input.contains("월세") || input.contains("보증금")) { + subType = "부동산"; + keywords = Arrays.asList("#임대차", "#보증금", "#월세"); + relatedLaws = Arrays.asList("주택임대차보호법", "민법 제618조 (임대차의 의의)"); + } + if (input.contains("형사") || input.contains("고소") || input.contains("사기")) { + classification = "형사"; + keywords = Arrays.asList("#사기", "#고소", "#형사처벌"); + relatedLaws = Arrays.asList("형법 제347조 (사기)", "형사소송법"); + } + + String summary = "온라인 쇼핑몰에서 구매한 전자제품을 7일 이내에 환불 요청했으나 판매자가 개봉을 이유로 거부하고 있는 상황입니다."; + String judgment = "온라인 쇼핑몰에서 구매한 전자제품을 7일 이내에 환불 요청했으나 판매자가 개봉을 이유로 거부하고 있는 상황입니다. 이는 전자상거래법상 보장되는 청약철회권과 판매자의 자체 규정이 충돌하는 분쟁입니다."; + + return new AnalysisResult(classification, subType, summary, keywords, judgment, relatedLaws); + } + + /** + * 분석 결과 내부 클래스 + */ + private static class AnalysisResult { + private final String classification; + private final String subType; + private final String summary; + private final List keywords; + private final String judgment; + private final List relatedLaws; + + public AnalysisResult(String classification, String subType, String summary, + List keywords, String judgment, List relatedLaws) { + this.classification = classification; + this.subType = subType; + this.summary = summary; + this.keywords = keywords; + this.judgment = judgment; + this.relatedLaws = relatedLaws; + } + + public String getClassification() { return classification; } + public String getSubType() { return subType; } + public String getSummary() { return summary; } + public List getKeywords() { return keywords; } + public String getJudgment() { return judgment; } + public List getRelatedLaws() { return relatedLaws; } + } +} diff --git a/legal/FrontEnd/my-react-app/component/footer.jsx b/legal/FrontEnd/my-react-app/component/footer.jsx index e69de29..dc7c7aa 100644 --- a/legal/FrontEnd/my-react-app/component/footer.jsx +++ b/legal/FrontEnd/my-react-app/component/footer.jsx @@ -0,0 +1,62 @@ +/** + * 공통 푸터 컴포넌트 + * + * [디자인] + * - 깔끔한 화이트 배경 + * - 미니멀한 스타일 + * - GitHub 링크 포함 + */ + +import { Link } from "react-router-dom"; + +export default function Footer() { + const currentYear = new Date().getFullYear(); + + return ( +
+
+
+ + {/* 로고 & 저작권 */} +
+ ⚖️ + + © {currentYear} LegalRisk AI + +
+ + {/* 링크 */} + + + {/* 연락처 및 GitHub */} +
+ 문의: support@legalrisk.ai + + + + + GitHub + +
+
+ + {/* 면책조항 */} +
+

+ 본 서비스는 법률 정보 제공 목적으로만 사용되며, 전문적인 법률 자문을 대체하지 않습니다. +

+
+
+
+ ); +} diff --git a/legal/FrontEnd/my-react-app/component/header.jsx b/legal/FrontEnd/my-react-app/component/header.jsx index e69de29..4318a36 100644 --- a/legal/FrontEnd/my-react-app/component/header.jsx +++ b/legal/FrontEnd/my-react-app/component/header.jsx @@ -0,0 +1,70 @@ +/** + * 공통 헤더 컴포넌트 + * + * [디자인] + * - 깔끔한 화이트 배경 + * - 미니멀한 스타일 + */ + +import { Link } from "react-router-dom"; + +export default function Header() { + return ( +
+
+
+ + {/* 로고 */} + + ⚖️ + LegalRisk AI + + + {/* 네비게이션 */} + + + {/* 로그인/회원가입 버튼 */} + +
+
+
+ ); +} diff --git a/legal/FrontEnd/my-react-app/src/App.jsx b/legal/FrontEnd/my-react-app/src/App.jsx index 6cd92a7..73c2167 100644 --- a/legal/FrontEnd/my-react-app/src/App.jsx +++ b/legal/FrontEnd/my-react-app/src/App.jsx @@ -1,26 +1,51 @@ -import { Routes, Route, Link } from "react-router-dom"; +/** + * 메인 앱 컴포넌트 + * + * [구조] + * - Header: 공통 헤더 (네비게이션) + * - Routes: 페이지 라우팅 + * - Footer: 공통 푸터 + */ +import { Routes, Route, Navigate } from "react-router-dom"; import Yusa from "./pages/yusa.jsx"; import Law from "./pages/law.jsx"; import Jogi from "./pages/jogi.jsx"; import Boonjang from "./pages/boonjang.jsx"; +import Header from "../component/header.jsx"; +import Footer from "../component/footer.jsx"; export default function App() { return ( -
- +
+ {/* 공통 헤더 */} +
- - } /> - } /> - } /> - } /> + {/* 메인 콘텐츠 */} +
+ + {/* 루트 경로 - 분쟁 페이지로 리다이렉트 */} + } /> + } /> + } /> + } /> + } /> + {/* 404 처리 */} + +
+

404

+

페이지를 찾을 수 없습니다

+ + 홈으로 돌아가기 + +
+
+ } /> + + - + {/* 공통 푸터 */} +
); } diff --git a/legal/FrontEnd/my-react-app/src/api/client.js b/legal/FrontEnd/my-react-app/src/api/client.js new file mode 100644 index 0000000..f57c64b --- /dev/null +++ b/legal/FrontEnd/my-react-app/src/api/client.js @@ -0,0 +1,70 @@ +/** + * API 클라이언트 헬퍼 함수 + * + * [원리 설명] + * - 모든 API 호출을 중앙화하여 일관된 에러 처리와 설정을 적용 + * - credentials: 'include'로 세션 쿠키(JSESSIONID)를 자동 전송 + * - Spring Security 세션 기반 인증과 연동 + * + * [왜 필요한가?] + * - 각 페이지에서 fetch를 직접 호출하면 코드 중복 발생 + * - HTTP 상태 코드 확인, JSON 파싱, 에러 처리가 일관되지 않음 + * - 환경별 API 베이스 URL 관리 필요 + */ + +// 환경 변수에서 API 베이스 URL 가져오기 (개발 시 Vite 프록시가 처리하므로 비워둠) +const API_BASE = import.meta.env.VITE_API_BASE || ''; + +/** + * GET 요청 헬퍼 + * @param {string} path - API 경로 (예: '/api/boonjang') + * @returns {Promise} - JSON 응답 데이터 + */ +export async function apiGet(path) { + const response = await fetch(`${API_BASE}${path}`, { + method: 'GET', + credentials: 'include', // 세션 쿠키 자동 전송 (JSP 로그인 연동) + headers: { + 'Content-Type': 'application/json', + }, + }); + + // HTTP 상태 코드 확인 + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + + // JSON 파싱 (응답이 비어있으면 null 반환) + const text = await response.text(); + return text ? JSON.parse(text) : null; +} + +/** + * POST 요청 헬퍼 + * @param {string} path - API 경로 (예: '/api/boonjang') + * @param {object} data - 전송할 데이터 + * @returns {Promise} - JSON 응답 데이터 + * + * [의도] + * - 분쟁 내용을 입력받아 백엔드로 전송 + * - DB에 저장하고 분석 결과를 반환받음 + */ +export async function apiPost(path, data) { + const response = await fetch(`${API_BASE}${path}`, { + method: 'POST', + credentials: 'include', // 세션 쿠키 자동 전송 + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + + const text = await response.text(); + return text ? JSON.parse(text) : null; +} diff --git a/legal/FrontEnd/my-react-app/src/api/queries.js b/legal/FrontEnd/my-react-app/src/api/queries.js new file mode 100644 index 0000000..f55ae93 --- /dev/null +++ b/legal/FrontEnd/my-react-app/src/api/queries.js @@ -0,0 +1,126 @@ +/** + * React Query 커스텀 훅 + * + * [원리 설명] + * React Query는 "서버 상태"를 관리하는 라이브러리입니다. + * + * 1. useQuery: 데이터 조회 (GET) + * - 자동으로 로딩 상태 관리 (isLoading) + * - 자동으로 에러 상태 관리 (error) + * - 자동으로 캐싱 (같은 요청 시 캐시 사용) + * - 자동으로 백그라운드 리페치 (탭 전환 시) + * + * 2. useMutation: 데이터 변경 (POST, PUT, DELETE) + * - 로딩 상태 관리 (isPending) + * - 에러 처리 + * - 성공 시 캐시 무효화 (다시 조회) + * + * [왜 필요한가?] + * 기존 코드: + * const [msg, setMsg] = useState(""); + * useEffect(() => { fetch(...).then(...) }, []); + * + * 문제점: + * - 로딩 상태 없음 + * - 에러 상세 정보 없음 + * - 캐싱 없음 (매번 재요청) + * - 코드 중복 + * + * React Query 사용 후: + * const { data, isLoading, error } = useBoonjangQuery(); + * → 모든 것이 자동으로 관리됨! + */ + +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiGet, apiPost } from './client'; + +// ============================================================ +// 분쟁 유형 관련 훅 (useBoonjang) +// ============================================================ + +/** + * 분쟁 목록 조회 훅 + * + * [사용법] + * const { data, isLoading, error } = useBoonjangQuery(); + * + * [반환값] + * - data: 서버에서 받은 분쟁 목록 + * - isLoading: 로딩 중이면 true + * - error: 에러 발생 시 에러 객체 + */ +export function useBoonjangQuery() { + return useQuery({ + queryKey: ['boonjang'], // 캐시 키 (같은 키면 캐시 재사용) + queryFn: () => apiGet('/api/boonjang'), // 실제 API 호출 함수 + staleTime: 1000 * 60 * 5, // 5분간 캐시 유지 (fresh 상태) + }); +} + +/** + * 분쟁 내용 분석 뮤테이션 + * + * [원리] + * useMutation은 데이터를 "변경"하는 작업에 사용합니다. + * - POST /api/boonjang 로 분쟁 내용 전송 + * - DB에 저장 + * - 분석 결과 반환 + * + * [사용법] + * const mutation = useBoonjangMutation(); + * + * // 버튼 클릭 시 + * mutation.mutate({ boonjangInput: "분쟁 내용..." }); + * + * // 상태 확인 + * if (mutation.isPending) return

분석 중...

; + * if (mutation.error) return

오류: {mutation.error.message}

; + * if (mutation.data) return <결과 컴포넌트 data={mutation.data} />; + */ +export function useBoonjangMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data) => apiPost('/api/boonjang', data), + onSuccess: () => { + // 성공 시 분쟁 목록 캐시 무효화 (다시 조회) + queryClient.invalidateQueries({ queryKey: ['boonjang'] }); + }, + }); +} + +// ============================================================ +// 법적 위험 관련 훅 (useLaw) +// ============================================================ + +export function useLawQuery() { + return useQuery({ + queryKey: ['law'], + queryFn: () => apiGet('/api/law'), + staleTime: 1000 * 60 * 5, + }); +} + +// ============================================================ +// 유사 판례 관련 훅 (useYusa) +// ============================================================ + +export function useYusaQuery() { + return useQuery({ + queryKey: ['yusa'], + queryFn: () => apiGet('/api/yusa'), + staleTime: 1000 * 60 * 5, + }); +} + +// ============================================================ +// 조기 위험 관련 훅 (useJogi) +// ============================================================ + +export function useJogiQuery() { + return useQuery({ + queryKey: ['jogi'], + queryFn: () => apiGet('/api/jogi'), + staleTime: 1000 * 60 * 5, + }); +} diff --git a/legal/FrontEnd/my-react-app/src/main.jsx b/legal/FrontEnd/my-react-app/src/main.jsx index 4e70272..8380221 100644 --- a/legal/FrontEnd/my-react-app/src/main.jsx +++ b/legal/FrontEnd/my-react-app/src/main.jsx @@ -1,11 +1,34 @@ +/** + * React 앱 진입점 + * + * [변경 사항] + * - QueryClientProvider 추가: React Query 활성화 + * + * [원리] + * QueryClientProvider는 React Query의 "클라이언트"를 모든 하위 컴포넌트에 제공합니다. + * 이를 통해 어떤 컴포넌트에서든 useQuery, useMutation을 사용할 수 있습니다. + */ import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import App from "./App.jsx"; -import './index.css' +import './index.css'; + +// React Query 클라이언트 생성 +// defaultOptions: 전역 기본 설정 (에러 시 재시도 횟수 등) +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, // 실패 시 1번 재시도 + refetchOnWindowFocus: false, // 창 포커스 시 자동 리페치 비활성화 + }, + }, +}); ReactDOM.createRoot(document.getElementById("root")).render( - - - + + + + + ); - \ No newline at end of file diff --git a/legal/FrontEnd/my-react-app/src/pages/boonjang.jsx b/legal/FrontEnd/my-react-app/src/pages/boonjang.jsx index 183c588..d7ebccb 100644 --- a/legal/FrontEnd/my-react-app/src/pages/boonjang.jsx +++ b/legal/FrontEnd/my-react-app/src/pages/boonjang.jsx @@ -1,45 +1,219 @@ +/** + * 분쟁 유형 분류 페이지 + * + * [구조] + * ┌────────────────────────────────────────────────────────┐ + * │ 분쟁 유형 분류 AI │ + * ├──────────────────────┬─────────────────────────────────┤ + * │ 분쟁 내용 입력 │ 분석 결과 │ + * │ (textarea) │ - 분류 유형 (민사/소비자) │ + * │ │ - 요약 │ + * │ [샘플 텍스트 입력] │ - 키워드 태그 │ + * │ [유형 분석 실행] │ - 법률적 판단 │ + * │ │ - 관련 법령 │ + * └──────────────────────┴─────────────────────────────────┘ + * + * [React Query 사용] + * - useBoonjangMutation: POST /api/boonjang 호출 + * - 자동 로딩/에러 상태 관리 + * + * [DB 저장 흐름] + * 1. 사용자가 분쟁 내용 입력 + * 2. "유형 분석 실행" 버튼 클릭 + * 3. POST /api/boonjang 호출 (boonjangInput 전송) + * 4. 백엔드가 DB에 저장 (BOONJANG 테이블) + * 5. 분석 결과 반환 → 오른쪽 패널에 표시 + */ + +import { useState } from "react"; import { Link } from "react-router-dom"; -import { useEffect, useState } from "react"; +import { useBoonjangMutation } from "../api/queries"; export default function Boonjang() { - const [msg, setMsg] = useState(""); + // 입력 상태 + const [inputText, setInputText] = useState(""); + + // React Query mutation (POST 요청) + const mutation = useBoonjangMutation(); + + // 샘플 텍스트 입력 + const handleSampleText = () => { + setInputText(`안녕하세요, 저는 온라인 쇼핑몰에서 전자제품을 구매했습니다. +제품 수령 후 3일 만에 청약철회를 요청했으나, 판매자가 개봉제품이라는 이유로 환불을 거부하고 있습니다. +전자상거래법에 따르면 7일 이내 청약철회가 가능한 것으로 알고 있는데, 판매자는 자체 규정을 근거로 반품 불가를 주장합니다. +이로 인해 50만원의 손해가 발생했으며, 부당한 청구라고 생각합니다. +소비자보호원에 신고할 예정이며, 필요시 법적 조치도 고려하고 있습니다.`); + }; - useEffect(() => { - fetch("/api/boonjang") - .then((r) => r.text()) - .then(setMsg) - .catch(() => setMsg("호출 실패")); - }, []); + // 분석 실행 + const handleAnalyze = () => { + if (!inputText.trim()) { + alert("분쟁 내용을 입력해주세요."); + return; + } + + // mutation.mutate()를 호출하면 POST 요청이 실행됨 + mutation.mutate({ + boonjangInput: inputText, + }); + }; return ( -
-
- {/* 제목 */} -

- Boonjang -

- - {/* 설명 */} -

- 분쟁 유형 분석 페이지 -

- - {/* API 메시지 영역 */} -
- {msg || "데이터를 불러오는 중..."} +
+ {/* 페이지 헤더 */} +
+
+
+ 📋 +
+
+

분쟁 유형 분류 AI

+

+ 분쟁 텍스트를 분석하여 Consumer, Contract, Administrative 등 유형을 자동 분류합니다. +

+
+
- {/* 구분선 */} -
+ {/* 메인 콘텐츠 */} +
+
- {/* 홈으로 이동 */} - - 홈으로 돌아가기 - -
+ {/* 왼쪽: 입력 패널 */} +
+

+ 📝 분쟁 내용 입력 +

+ +