Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6d8202c
feat[email]: 이메일 전송 로직 구현
asowjdan Sep 25, 2025
8ac8732
feat[email]: 이메일 전송 DTO 구현
asowjdan Sep 25, 2025
3374cce
feat[email]: 비밀번호 재설정 로직 구현
asowjdan Sep 25, 2025
f5ba176
feat[corsConfiguration]: 운영 서버 도메인에 대한 cors 설정 추가
asowjdan Sep 25, 2025
420fb32
test[member]: 이메일 전송 및 비밀번호 재설정 기능 테스트 코드 추가
asowjdan Sep 25, 2025
47296e8
ci[test]: ci 테스트의 환경변수 세팅 수정 및 Repository secrets 세팅
asowjdan Sep 25, 2025
0650308
ci[test]: ci 테스트의 환경변수 세팅을 다시 로컬 테스트 세팅처럼 수정
asowjdan Sep 25, 2025
c0175e5
ci[test]: ci 테스트의 환경변수 세팅을 테스트 성공 기점으로 롤백 이후 이메일 환경변수만 추가
asowjdan Sep 25, 2025
7ba260a
ci[test]: ci 테스트의 환경변수 세팅을 테스트 성공 기점으로 롤백 이후 이메일 환경변수만 추가
asowjdan Sep 25, 2025
80f201b
Merge pull request #87 from asowjdan/feat/41-member
DooHyoJeong Sep 26, 2025
17021d2
feat[law-word]:법령 검색 기능 추가
Nohheechul Sep 26, 2025
b5a2cc0
Merge pull request #89 from prgrms-web-devcourse-final-project/feat/8…
Nohheechul Sep 26, 2025
cf3a810
fix[member]: 비밀번호 재설정 컨트롤러의 입출력 형식 수정
asowjdan Sep 26, 2025
b8fd3a3
fix[member]: 비밀번호 재설정 인증번호 체크여부 검증 로직 추가
asowjdan Sep 26, 2025
9e664c4
docs[member]: swagger 문서에 출력되는 컨트롤러 순서 지정 코드 추가
asowjdan Sep 26, 2025
c809694
refactor[docker]: redis 추가
DooHyoJeong Sep 26, 2025
92fea5c
Merge pull request #91 from prgrms-web-devcourse-final-project/chore/…
DooHyoJeong Sep 26, 2025
7918e4f
test[member]: 이메일 인증 로직 수정으로 인한 테스트 코드 수정
asowjdan Sep 26, 2025
e4a744b
Merge branch 'develop' into feat/41-member
asowjdan Sep 26, 2025
21bde78
refactor[docker]: root 유저 생성 이슈 해결
DooHyoJeong Sep 26, 2025
e56459e
fix[member]: 로컬 레디스로 연결될 때 로그 추가
asowjdan Sep 26, 2025
23eda86
fix[member]: 개발 환경 로컬 레디스로 연결
asowjdan Sep 26, 2025
54f7294
Merge pull request #92 from prgrms-web-devcourse-final-project/chore/…
asowjdan Sep 26, 2025
7629b7c
Merge branch 'develop' into feat/41-member
asowjdan Sep 26, 2025
52a60eb
Merge pull request #90 from asowjdan/feat/41-member
DooHyoJeong Sep 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/CI-CD_Pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ jobs:

# JPA 설정 (application-test.yml에서 참조)
TEST_JPA_HIBERNATE_DDL_AUTO=create-drop

email_address=${{ secrets.EMAIL_ADDRESS }}
send_email_password=${{ secrets.EMAIL_PASSWORD }}
send_email_address=${{ secrets.SEND_EMAIL_ADDRESS }}

# Redis 설정 (application-test.yml에서 참조, GitHub Actions 서비스 사용)
TEST_REDIS_HOST=localhost
Expand Down Expand Up @@ -204,7 +208,7 @@ jobs:
PROD_DATASOURCE_URL=jdbc:mysql://mysql_1:3306/${{ secrets.DB_NAME }}
PROD_DATASOURCE_USERNAME=${{ secrets.DB_USER }}
PROD_DATASOURCE_PASSWORD=${{ secrets.DB_PASSWORD }}

PROD_REDIS_HOST=redis_1
PROD_REDIS_PORT=6379
PROD_REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}
Expand Down Expand Up @@ -232,15 +236,15 @@ jobs:
PROD_DATASOURCE_DRIVER=com.mysql.cj.jdbc.Driver
PROD_DATASOURCE_USERNAME=root
PROD_DATASOURCE_PASSWORD=${{ secrets.DB_PASSWORD }}

PROD_REDIS_HOST=redis_1
PROD_REDIS_PORT=6379
PROD_REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}
EOF

# EC2에서 GHCR 로그인
echo "${{ secrets.GHCR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin

# 최신 이미지 pull & 컨테이너 실행
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ env.DOCKER_IMAGE_NAME }}:latest
docker stop app1 2>/dev/null || true
Expand Down
1 change: 1 addition & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '3.0.5'

// API Documentation (문서화)
implementation 'org.apache.commons:commons-lang3:3.18.0'
Expand Down
30 changes: 23 additions & 7 deletions backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ services:
container_name: mysql
restart: unless-stopped
environment:
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASS}
MYSQL_DATABASE: balaw
MYSQL_PASSWORD: ${DEV_DATASOURCE_PASSWORD}
# 새 변수: 루트 계정 비번 (필수)
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
MYSQL_ROOT_PASSWORD: ${DEV_DB_ROOT_PASSWORD}
TZ: Asia/Seoul
ports:
# 호스트 포트는 원하는 값으로, 컨테이너는 3306 고정
- "${DB_PORT:-3306}:3306"
- "${DEV_DATASOURCE_PORT:-3306}:3306"
volumes:
- mysql-data:/var/lib/mysql
command: >
Expand All @@ -23,10 +22,27 @@ services:
--default-time-zone=Asia/Seoul
--skip-log-bin
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-p${DB_ROOT_PASS}"]
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-p${DEV_DATASOURCE_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 10

redis:
image: redis:7.4
container_name: redis
restart: unless-stopped
ports:
- "${DEV_REDIS_PORT:-6379}:6379"
volumes:
- redis-data:/data
command: >
redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10

volumes:
mysql-data:
mysql-data:
redis-data:
13 changes: 13 additions & 0 deletions backend/sql/drop.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
drop table ho;
drop table hang;
drop table jo;
drop table jang;
drop table law;
drop table precedent;

ALTER TABLE ho AUTO_INCREMENT = 1;
ALTER TABLE hang AUTO_INCREMENT = 1;
ALTER TABLE jo AUTO_INCREMENT = 1;
ALTER TABLE jang AUTO_INCREMENT = 1;
ALTER TABLE law AUTO_INCREMENT = 1;
ALTER TABLE precedent AUTO_INCREMENT = 1;
150,586 changes: 150,586 additions & 0 deletions backend/sql/lawData.sql

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,22 @@
import com.ai.lawyer.domain.law.entity.Law;
import com.ai.lawyer.domain.law.service.LawService;
import com.ai.lawyer.global.dto.PageResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/law")
@RequestMapping("/api/law")
public class LawController {

private final LawService lawService;

// 법령 리스트 출력
@GetMapping(value = "/list")
public ResponseEntity<?> list(
@RequestParam String query,
@RequestParam int page
) throws Exception {
String lawList = lawService.getLawList(query, page);
return ResponseEntity.ok().body(lawList);
}


@GetMapping(value = "/list/save")
@Operation(summary = "키워드 관련 법령 데이터 저장(벡엔드 전용 API)", description = "벡엔드 데이터 저장용 API입니다")
public ResponseEntity<?> getStatisticsCard(
@RequestParam String query,
@RequestParam int page
Expand All @@ -44,15 +36,8 @@ public ResponseEntity<?> getStatisticsCard(
return ResponseEntity.ok().body("Success");
}

@GetMapping("/{id}")
public ResponseEntity<Law> getFullLaw(@PathVariable Long id) {
Law law = lawService.getLawWithAllChildren(id);

return ResponseEntity.ok(law);
}


@PostMapping("/search")
@Operation(summary = "볍령 목록 검색 기능", description = "조건에 맞는 법령 목록을 가져옵니다")
public ResponseEntity<PageResponseDto> searchLaws(@RequestBody LawSearchRequestDto searchRequest) {
Page<LawsDto> laws = lawService.searchLaws(searchRequest);
PageResponseDto response = PageResponseDto.builder()
Expand All @@ -64,4 +49,13 @@ public ResponseEntity<PageResponseDto> searchLaws(@RequestBody LawSearchRequestD
.build();
return ResponseEntity.ok(response);
}

@GetMapping("/{id}")
@Operation(summary = "볍령 상세 조회 기능", description = "법령 상세 데이터를 조회합니다 \n" +
"예시: /api/law/1")
public ResponseEntity<Law> getFullLaw(@PathVariable Long id) {
Law law = lawService.getLawWithAllChildren(id);

return ResponseEntity.ok(law);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ai.lawyer.domain.law.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;

Expand All @@ -8,13 +9,31 @@
@Data
@Builder
public class LawSearchRequestDto {

@Schema(description = "법령명", example = "노동")
private String lawName; // 법령명

@Schema(description = "법령분야", example = "법률")
private String lawField; // 법령분야

@Schema(description = "소관부처", example = "고용노동부")
private String ministry; // 소관부처

@Schema(description = "공포일자 시작", example = "2000-03-25")
private LocalDate promulgationDateStart; // 공포일자 시작

@Schema(description = "공포일자 종료", example = "2025-03-25")
private LocalDate promulgationDateEnd; // 공포일자 종료

@Schema(description = "시행일자 시작", example = "2000-03-25")
private LocalDate enforcementDateStart; // 시행일자 시작

@Schema(description = "시행일자 종료", example = "2025-03-25")
private LocalDate enforcementDateEnd; // 시행일자 종료

@Schema(description = "페이지 번호 (0부터 시작)", example = "0")
private int pageNumber; // 페이지 번호

@Schema(description = "페이지 크기", example = "10")
private int pageSize; // 페이지 크기
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.ai.lawyer.domain.lawWord.controller;

import com.ai.lawyer.domain.lawWord.service.LawWordService;
import com.ai.lawyer.domain.precedent.entity.Precedent;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/law-word")
public class LawWordController {

private final LawWordService lawWordService;

@GetMapping("/{word}")
@Operation(summary = "법령 용어 검색", description = "법령 용어에 대한 정의를 반환합니다. \n" +
"예시: /api/law-word/선박")
public ResponseEntity<?> getPrecedent(@PathVariable String word) {
return ResponseEntity.ok(lawWordService.findDefinition(word));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.ai.lawyer.domain.lawWord.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LawWord {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@JsonIgnore
private Long id;

private String word; // 법률 용어

@Lob
@Column(columnDefinition = "LONGTEXT")
private String definition; // 정의

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ai.lawyer.domain.lawWord.repository;

import com.ai.lawyer.domain.lawWord.entity.LawWord;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface LawWordRepository extends JpaRepository<LawWord, Long> {
Optional<LawWord> findByWord(String word);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.ai.lawyer.domain.lawWord.service;

import com.ai.lawyer.domain.lawWord.entity.LawWord;
import com.ai.lawyer.domain.lawWord.repository.LawWordRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
@AllArgsConstructor
public class LawWordService {

private final LawWordRepository lawWordRepository;

private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();

public String findDefinition(String word) {
// 1) DB 확인
return lawWordRepository.findByWord(word)
.map(LawWord::getDefinition)
.orElseGet(() -> {
try {
String url = "https://www.law.go.kr/DRF/lawService.do?OC=noheechul"
+ "&target=lstrm&type=JSON&query=" + word;
String json = restTemplate.getForObject(url, String.class);
JsonNode rootNode = objectMapper.readTree(json);
// 오류 응답 처리
if (rootNode.has("Law")) {
return rootNode.get("Law").asText();
}
JsonNode serviceNode = rootNode.path("LsTrmService");
JsonNode defNode = serviceNode.path("법령용어정의");
String definition;
if (defNode.isArray() && defNode.size() > 0) {
// definition = defNode.get(0).asText().split("\\.",2)[0].trim();
definition = defNode.get(0).asText().trim();
} else {
// definition = defNode.asText().split("\\.",2)[0].trim();
definition = defNode.asText().trim();
}
LawWord entity = LawWord.builder()
.word(word)
.definition(definition)
.build();
lawWordRepository.save(entity);
return definition;
} catch (Exception e) {
throw new RuntimeException("Failed to fetch definition", e);
}
});
}
}
Loading