diff --git a/.DS_Store b/.DS_Store index d8d6220..7cf982e 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 0a253a8..b5fb2fc 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ out/ ### VS Code ### .vscode/ +.DS_Store diff --git a/Task b/Task new file mode 100644 index 0000000..e69de29 diff --git a/build.gradle b/build.gradle index c64a101..dbd656b 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,24 @@ dependencies { // testImplementation 'org.springframework.security:spring-security-test' } + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.jsoup:jsoup:1.15.3' + runtimeOnly 'com.h2database:h2' // 예제에서는 H2 in-memory DB 사용 + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + + + + tasks.named('test') { useJUnitPlatform() } + +dependencies { + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.133' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' +} \ No newline at end of file diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000..846e5e5 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/main/java/com/leafvillage/backend/Controller/CompanyController.java b/src/main/java/com/leafvillage/backend/Controller/CompanyController.java new file mode 100644 index 0000000..a12c80b --- /dev/null +++ b/src/main/java/com/leafvillage/backend/Controller/CompanyController.java @@ -0,0 +1,44 @@ +package com.leafvillage.backend.Controller; + +import com.leafvillage.backend.entity.Company; +import com.leafvillage.backend.repository.CompanyRepository; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.util.List; +import java.util.stream.Collectors; + +@RestController +public class CompanyController { + + private final CompanyRepository companyRepository; + + public CompanyController(CompanyRepository companyRepository) { + this.companyRepository = companyRepository; + } + + /** + * 기업 리스트 조회 (1~20개 반환) + */ + @GetMapping("/companies") + public List getCompanies( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + + // 페이징 적용: 최신 등록 순으로 20개만 가져오기 + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.ASC, "companyId")); + List companies = companyRepository.findAll(pageable).getContent(); + + // 필요한 데이터만 DTO로 변환하여 반환 + return companies.stream() + .map(company -> new CompanyDto(company.getCompanyId(), company.getCompany())) + .collect(Collectors.toList()); + } + + // DTO 클래스 정의 + public record CompanyDto(String company_id, String company) {} +} diff --git a/src/main/java/com/leafvillage/backend/Controller/Test/DatabaseTestController.java b/src/main/java/com/leafvillage/backend/Controller/Test/DatabaseTestController.java deleted file mode 100644 index 153d538..0000000 --- a/src/main/java/com/leafvillage/backend/Controller/Test/DatabaseTestController.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.leafvillage.backend.Controller.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/db") -public class DatabaseTestController { - - @Autowired - private JdbcTemplate jdbcTemplate; - - @GetMapping("/test") - public ResponseEntity testDatabaseConnection() { - try { - jdbcTemplate.queryForObject("SELECT 1", Integer.class); - return ResponseEntity.ok("RDS 연결 성공 (Spring Boot)"); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("RDS 연결 실패: " + e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/leafvillage/backend/Controller/Test/StatusController.java b/src/main/java/com/leafvillage/backend/Controller/Test/StatusController.java deleted file mode 100644 index 58dcb68..0000000 --- a/src/main/java/com/leafvillage/backend/Controller/Test/StatusController.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.leafvillage.backend.Controller.Test; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/status") -public class StatusController { - - @GetMapping - public ResponseEntity checkStatus() { - return ResponseEntity.ok("백엔드 서버 정상 작동 중!"); - } -} \ No newline at end of file diff --git a/src/main/java/com/leafvillage/backend/dto/CompanyPartialDto.java b/src/main/java/com/leafvillage/backend/dto/CompanyPartialDto.java new file mode 100644 index 0000000..ea672c4 --- /dev/null +++ b/src/main/java/com/leafvillage/backend/dto/CompanyPartialDto.java @@ -0,0 +1,37 @@ +package com.leafvillage.backend.dto; + +public class CompanyPartialDto { + private String corpNm; // 회사명 + private String englishName; // 영문명 + private String address; // 주소 + + public CompanyPartialDto() { + } + + public CompanyPartialDto(String corpNm, String englishName, String address) { + this.corpNm = corpNm; + this.englishName = englishName; + this.address = address; + } + + public String getCorpNm() { + return corpNm; + } + public void setCorpNm(String corpNm) { + this.corpNm = corpNm; + } + + public String getEnglishName() { + return englishName; + } + public void setEnglishName(String englishName) { + this.englishName = englishName; + } + + public String getAddress() { + return address; + } + public void setAddress(String address) { + this.address = address; + } +} diff --git a/src/main/java/com/leafvillage/backend/entity/Company.java b/src/main/java/com/leafvillage/backend/entity/Company.java new file mode 100644 index 0000000..0d9b580 --- /dev/null +++ b/src/main/java/com/leafvillage/backend/entity/Company.java @@ -0,0 +1,167 @@ +package com.leafvillage.backend.entity; + +import jakarta.persistence.*; +import java.util.Date; + +@Entity +@Table(name = "company") +public class Company { + + @Id + @Column(name = "company_id", length = 36, nullable = false) + private String companyId; // 예: "c000001" + + @Column(name = "company", length = 255, nullable = false) + private String company; // 회사 고유 식별자 + + @Column(name = "company_name_kr", length = 255) + private String companyNameKr; // 회사명(국문) + + @Column(name = "representative_name", length = 100) + private String representativeName; // 대표자명 + + @Column(name = "business_registration_number", length = 100) + private String businessRegistrationNumber; // 사업자등록번호 + + @Column(name = "address", length = 255) + private String address; // 주소 + + @Column(name = "phone_number", length = 255) + private String phoneNumber; // 전화번호 + + @Column(name = "fax_number", length = 255) + private String faxNumber; // 팩스번호 + + @Column(name = "homepage_url", length = 255) + private String homepageUrl; // 홈페이지 URL + + @Column(name = "standard_industry_classification", length = 255) + private String standardIndustryClassification; // 표준산업분류 + + @Column(name = "main_business", length = 255) + private String mainBusiness; // 주요사업 + + @Column(name = "establishment_date") + @Temporal(TemporalType.DATE) + private Date establishmentDate; // 설립일 + + @Column(name = "kosdaq_listed_date") + @Temporal(TemporalType.DATE) + private Date kosdaqListedDate; // 코스닥 상장일 + + @Column(name = "ticker", length = 20) + private String ticker; // 티커 + + @Column(name = "logo_image", length = 255) + private String logoImage; // 이미지 링크 + + // 기본 생성자 + public Company() {} + + // Getter/Setter + + public String getCompanyId() { + return companyId; + } + public void setCompanyId(String companyId) { + this.companyId = companyId; + } + + public String getCompany() { + return company; + } + public void setCompany(String company) { + this.company = company; + } + + public String getCompanyNameKr() { + return companyNameKr; + } + public void setCompanyNameKr(String companyNameKr) { + this.companyNameKr = companyNameKr; + } + + public String getRepresentativeName() { + return representativeName; + } + public void setRepresentativeName(String representativeName) { + this.representativeName = representativeName; + } + + public String getBusinessRegistrationNumber() { + return businessRegistrationNumber; + } + public void setBusinessRegistrationNumber(String businessRegistrationNumber) { + this.businessRegistrationNumber = businessRegistrationNumber; + } + + public String getAddress() { + return address; + } + public void setAddress(String address) { + this.address = address; + } + + public String getPhoneNumber() { + return phoneNumber; + } + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public String getFaxNumber() { + return faxNumber; + } + public void setFaxNumber(String faxNumber) { + this.faxNumber = faxNumber; + } + + public String getHomepageUrl() { + return homepageUrl; + } + public void setHomepageUrl(String homepageUrl) { + this.homepageUrl = homepageUrl; + } + + public String getStandardIndustryClassification() { + return standardIndustryClassification; + } + public void setStandardIndustryClassification(String standardIndustryClassification) { + this.standardIndustryClassification = standardIndustryClassification; + } + + public String getMainBusiness() { + return mainBusiness; + } + public void setMainBusiness(String mainBusiness) { + this.mainBusiness = mainBusiness; + } + + public Date getEstablishmentDate() { + return establishmentDate; + } + public void setEstablishmentDate(Date establishmentDate) { + this.establishmentDate = establishmentDate; + } + + public Date getKosdaqListedDate() { + return kosdaqListedDate; + } + public void setKosdaqListedDate(Date kosdaqListedDate) { + this.kosdaqListedDate = kosdaqListedDate; + } + + public String getTicker() { + return ticker; + } + public void setTicker(String ticker) { + this.ticker = ticker; + } + + public String getLogoImage() { + return logoImage; + } + public void setLogoImage(String logoImage) { + this.logoImage = logoImage; + } +} diff --git a/src/main/java/com/leafvillage/backend/repository/CompanyRepository.java b/src/main/java/com/leafvillage/backend/repository/CompanyRepository.java new file mode 100644 index 0000000..65c46d5 --- /dev/null +++ b/src/main/java/com/leafvillage/backend/repository/CompanyRepository.java @@ -0,0 +1,9 @@ +package com.leafvillage.backend.repository; +import java.util.Optional; +import com.leafvillage.backend.entity.Company; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CompanyRepository extends JpaRepository { + // 기본 제공되는 findById(companyId) 사용 (JPA에서 자동 제공) + Optional findByCompany(String company); +} diff --git a/src/main/java/com/leafvillage/backend/runner/CompanyApiRunner.java b/src/main/java/com/leafvillage/backend/runner/CompanyApiRunner.java new file mode 100644 index 0000000..4506c39 --- /dev/null +++ b/src/main/java/com/leafvillage/backend/runner/CompanyApiRunner.java @@ -0,0 +1,85 @@ +package com.leafvillage.backend.runner; + +import com.leafvillage.backend.service.CompanyApiService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class CompanyApiRunner implements CommandLineRunner { + + @Autowired + private CompanyApiService companyApiService; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void run(String... args) { + // ✅ 하드코딩된 회사명 (테스트할 기업 리스트) + List corpNames = List.of("삼성전자", "현대자동차", "카카오"); + + for (String corpNm : corpNames) { + System.out.println("조회할 기업명: " + corpNm); + + try { + // API 호출 후 JSON 응답 받기 + String fullJson = companyApiService.getCompanyDataByName(corpNm); + JsonNode root = objectMapper.readTree(fullJson); + JsonNode itemsNode = root.path("response").path("body").path("items").path("item"); + + // 기업명 정규화 및 (주) 관련 케이스 구분 + String normalizedInput = corpNm.replaceAll("\\s+", ""); + String variant1 = normalizedInput; + String variant2 = "(주)" + normalizedInput; + String variant3 = normalizedInput + "(주)"; + + List exactMatches = new ArrayList<>(); + if (itemsNode.isArray()) { + for (JsonNode item : itemsNode) { + String name = item.path("corpNm").asText("").replaceAll("\\s+", ""); + if (name.equals(variant1) || name.equals(variant2) || name.equals(variant3)) { + exactMatches.add(item); + } + } + } else if (!itemsNode.isMissingNode()) { + String name = itemsNode.path("corpNm").asText("").replaceAll("\\s+", ""); + if (name.equals(variant1) || name.equals(variant2) || name.equals(variant3)) { + exactMatches.add(itemsNode); + } + } + + // 결과 출력 + if (exactMatches.isEmpty()) { + System.out.println(corpNm + " : 정확한 일치 항목 없음"); + } else { + System.out.println("=== 정확하게 일치하는 항목 (" + exactMatches.size() + "건) ==="); + for (JsonNode item : exactMatches) { + String companyName = item.path("corpNm").asText("회사명 없음"); + String address = item.path("enpBsadr").asText("주소 없음"); + String phone = item.path("enpTlno").asText("전화번호 없음"); + String domain = item.path("enpHmpgUrl").asText("도메인 없음"); + + System.out.println("회사명: " + companyName); + System.out.println("주소: " + address); + System.out.println("전화번호: " + phone); + System.out.println("도메인: " + domain); + if (!domain.equals("도메인 없음")) { + String logoUrl = "https://logo.clearbit.com/" + domain; + System.out.println("Clearbit 로고 URL: " + logoUrl); + } else { + System.out.println("Clearbit 로고 URL: 도메인 정보 없음"); + } + System.out.println("-----"); + } + } + } catch (Exception e) { + System.out.println("API 조회 중 오류 발생: " + e.getMessage()); + } + } + } +} diff --git a/src/main/java/com/leafvillage/backend/service/CompanyApiService.java b/src/main/java/com/leafvillage/backend/service/CompanyApiService.java new file mode 100644 index 0000000..a832101 --- /dev/null +++ b/src/main/java/com/leafvillage/backend/service/CompanyApiService.java @@ -0,0 +1,108 @@ +package com.leafvillage.backend.service; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.JsonNode; +import java.util.*; +@Service +public class CompanyApiService { + + // 이미 인코딩된 서비스키를 사용 (브라우저에서 테스트한 URL과 동일) + private static final String SERVICE_KEY = "YugI7KFgMPZzeCgNekIYquyzw8WQVtkPJfccnooevCyF5Rh5ZXaL92U26v6PxVdapMNXtf5XfzvajTKbGkfuew=="; + private static final String BASE_URL = "https://apis.data.go.kr/1160100/service/GetCorpBasicInfoService_V2/getCorpOutline_V2"; + + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + + public CompanyApiService() { + this.restTemplate = new RestTemplate(); + this.objectMapper = new ObjectMapper(); + } + + /** + * 프론트엔드에서 한글 회사명을 전달받아, 공공데이터포털 API를 호출한 후 JSON 응답을 반환합니다. + */ + public String getCompanyDataByName(String corpNm) { + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(BASE_URL) + .queryParam("serviceKey", SERVICE_KEY) // 대소문자도 브라우저와 동일하게 + .queryParam("pageNo", 1) + .queryParam("numOfRows", 1) // 아마 이 부분에서 "=== 정확하게 일치하는 항목 (5건) ===" 문구가 반복적 출력되는 듯 + .queryParam("resultType", "json") + .queryParam("corpNm", corpNm); + + // build(false)를 사용하여 이미 인코딩된 파라미터가 재인코딩되지 않도록 합니다. + String url = builder.build(false).toUriString(); + System.out.println("요청 URL: " + url); // 디버그용 로그 + + ResponseEntity response = restTemplate.getForEntity(url, String.class); + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { + // JSON 파싱 + String json = response.getBody(); + System.out.println("==============================================================="); + System.out.println(json); + System.out.println("==============================================================="); + try{ + JsonNode root = objectMapper.readTree(json); + JsonNode itemsNode = root.path("response") + .path("body") + .path("items") + .path("item"); + + // 정확한 매칭을 위한 리스트 + String normalizedInput = corpNm.replaceAll("\\s+", "").trim(); + String variant1 = normalizedInput; + String variant2 = "(주)" + normalizedInput; + String variant3 = normalizedInput + "(주)"; + + List exactMatches = new ArrayList<>(); + if (itemsNode.isArray()) { + for (JsonNode item : itemsNode) { + String name = item.path("corpNm").asText("").replaceAll("\\s+", ""); + if (name.equals(variant1) || name.equals(variant2) || name.equals(variant3)) { + exactMatches.add(item); + } + } + }else if (!itemsNode.isMissingNode()) { + // 단일 객체인 경우에도 일치 여부 확인 + String name = itemsNode.path("corpNm").asText("").replaceAll("\\s+", "").trim(); + if (name.equals(variant1) || name.equals(variant2) || name.equals(variant3)) { + exactMatches.add(itemsNode); + } + } + + if (exactMatches.isEmpty()) { + System.out.println("정확한 일치 항목이 없습니다."); + return "{}"; + } else { + System.out.println("=== 정확하게 일치하는 항목 (" + exactMatches.size() + "건) ==="); + for (JsonNode item : exactMatches) { + String address = item.path("enpBsadr").asText("주소 없음"); + String phone = item.path("enpTlno").asText("전화번호 없음"); + String domain = item.path("enpDmn").asText("도메인 없음"); + + //System.out.println("-----"); + //System.out.println("회사명: " + item.path("corpNm").asText("회사명 없음")); + //System.out.println("주소: " + address); + //System.out.println("전화번호: " + phone); + //System.out.println("도메인: " + domain); + //if (!domain.equals("도메인 없음")) { + // String logoUrl = "https://logo.clearbit.com/" + domain; + // System.out.println("Clearbit 로고 URL: " + logoUrl); + //} else { + // System.out.println("Clearbit 로고 URL: 도메인 정보가 없어 생성할 수 없습니다."); + //} + } + } + + }catch(Exception e){ + throw new RuntimeException("JSON 파싱 오류: " + e.getMessage()); + } + return json; + } else { + throw new RuntimeException("API 호출 실패: " + response.getStatusCodeValue()); + } + } +} diff --git a/src/main/java/com/leafvillage/backend/standalone/CompanyApiStandaloneRunner.java b/src/main/java/com/leafvillage/backend/standalone/CompanyApiStandaloneRunner.java new file mode 100644 index 0000000..224d8c5 --- /dev/null +++ b/src/main/java/com/leafvillage/backend/standalone/CompanyApiStandaloneRunner.java @@ -0,0 +1,96 @@ +package com.leafvillage.backend.standalone; + +import com.leafvillage.backend.service.CompanyApiService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class CompanyApiStandaloneRunner { + public static void main(String[] args) { + CompanyApiService service = new CompanyApiService(); + ObjectMapper mapper = new ObjectMapper(); + Scanner scanner = new Scanner(System.in); + + System.out.println("조회할 기업명을 입력하세요:"); + String corpNm = scanner.hasNextLine() ? scanner.nextLine() : "기본기업명"; + + try { + // API 호출 후 전체 JSON 응답 받기 + String fullJson = service.getCompanyDataByName(corpNm); + + // 1) 전체 JSON을 콘솔에 출력 (디버그/확인용) + System.out.println("----- 전체 JSON 응답 -----"); + System.out.println(fullJson); + System.out.println("----- 끝 -----"); + + // 2) JSON 파싱 + JsonNode root = mapper.readTree(fullJson); + JsonNode itemsNode = root.path("response") + .path("body") + .path("items") + .path("item"); + + // 기업명 정규화 및 (주) 관련 케이스 구분 + String normalizedInput = corpNm.replaceAll("\\s+", ""); + String variant1 = normalizedInput; + String variant2 = "(주)" + normalizedInput; + String variant3 = normalizedInput + "(주)"; + + List exactMatches = new ArrayList<>(); + if (itemsNode.isArray()) { + for (JsonNode item : itemsNode) { + String name = item.path("corpNm").asText("").replaceAll("\\s+", ""); + if (name.equals(variant1) || name.equals(variant2) || name.equals(variant3)) { + exactMatches.add(item); + } + } + } else if (!itemsNode.isMissingNode()) { + String name = itemsNode.path("corpNm").asText("").replaceAll("\\s+", ""); + if (name.equals(variant1) || name.equals(variant2) || name.equals(variant3)) { + exactMatches.add(itemsNode); + } + } + + // 결과 출력 + if (exactMatches.isEmpty()) { + System.out.println(corpNm + " : 정확한 일치 항목 없음"); + } else { + System.out.println("=== 정확하게 일치하는 항목 (" + exactMatches.size() + "건) ==="); + for (JsonNode item : exactMatches) { + // 주요 필드 추출 + String companyName = item.path("corpNm").asText("회사명 없음"); + String address = item.path("enpBsadr").asText("주소 없음"); + String phone = item.path("enpTlno").asText("전화번호 없음"); + String domain = item.path("enpHmpgUrl").asText("도메인 없음"); + + // 추가로 대표자명, 사업자등록번호, 주요사업 등 더 파싱 (필드명은 API 문서 확인 필요) + String representative = item.path("enpRprFnm").asText("N/A"); // 대표자명 + String bsnNo = item.path("enpBsnNo").asText("N/A"); // 사업자등록번호 + String mainBiz = item.path("enpMainbizNm").asText("N/A"); // 주요사업 + + System.out.println("회사명: " + companyName); + System.out.println("대표자명: " + representative); + System.out.println("사업자등록번호: " + bsnNo); + System.out.println("주요사업: " + mainBiz); + System.out.println("주소: " + address); + System.out.println("전화번호: " + phone); + System.out.println("도메인: " + domain); + + if (!domain.equals("도메인 없음")) { + String logoUrl = "https://logo.clearbit.com/" + domain; + System.out.println("Clearbit 로고 URL: " + logoUrl); + } else { + System.out.println("Clearbit 로고 URL: 도메인 정보 없음"); + } + System.out.println("-----"); + } + } + } catch (Exception e) { + System.out.println("API 조회 중 오류 발생: " + e.getMessage()); + } finally { + scanner.close(); + } + } +}