release: 2026-03-31 (40건 커밋) #118

병합
HYOJIN develop 에서 main 로 40 commits 를 머지했습니다 2026-03-31 11:09:31 +09:00
9개의 변경된 파일262개의 추가작업 그리고 43개의 파일을 삭제
Showing only changes of commit b47b5050fd - Show all commits

파일 보기

@ -72,6 +72,8 @@ export interface ShipInfoResponse {
imoNo: string;
shipName: string;
shipStatus: string;
nationalityCode: string;
nationalityIsoCode: string | null;
nationality: string;
shipType: string;
dwt: string;
@ -87,11 +89,19 @@ export interface CompanyInfoResponse {
companyCode: string;
fullName: string;
abbreviation: string;
country: string;
city: string;
status: string;
parentCompanyCode: string | null;
parentCompanyName: string | null;
registrationCountry: string;
address: string;
registrationCountryCode: string;
registrationCountryIsoCode: string | null;
controlCountry: string | null;
controlCountryCode: string | null;
controlCountryIsoCode: string | null;
foundedDate: string | null;
email: string | null;
phone: string | null;
website: string | null;
}
// 지표 현재 상태

파일 보기

@ -73,13 +73,11 @@ function StatusBadge({ value }: { value: string | null }) {
);
}
function InfoField({ label, value }: { label: string; value: string | null | undefined }) {
return (
<div>
<div className="text-wing-muted mb-0.5">{label}</div>
<div className="font-medium text-wing-text">{value || '-'}</div>
</div>
);
function countryFlag(code: string | null | undefined): string {
if (!code || code.length < 2) return '';
const cc = code.slice(0, 2).toUpperCase();
const codePoints = [...cc].map((c) => 0x1F1E6 + c.charCodeAt(0) - 65);
return String.fromCodePoint(...codePoints);
}
function RiskValueCell({ value, narrative }: { value: string | null; narrative?: string }) {
@ -547,29 +545,137 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
{expandedSections.has('info') && (
<div className="border-t border-wing-border px-4 py-4">
{shipInfo && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
<InfoField label="선박명" value={shipInfo.shipName} />
<InfoField label="IMO" value={shipInfo.imoNo} />
<InfoField label="MMSI" value={shipInfo.mmsiNo} />
<InfoField label="호출부호" value={shipInfo.callSign} />
<InfoField label="국적" value={shipInfo.nationality} />
<InfoField label="선종" value={shipInfo.shipType} />
<InfoField label="DWT" value={shipInfo.dwt} />
<InfoField label="GT" value={shipInfo.gt} />
<InfoField label="건조연도" value={shipInfo.buildYear} />
<InfoField label="상태" value={shipInfo.shipStatus} />
<div className="flex gap-6">
{/* 좌측: 핵심 식별 정보 */}
<div className="min-w-[220px] space-y-2">
<div>
<div className="text-lg font-bold text-wing-text">{shipInfo.shipName || '-'}</div>
</div>
<div className="space-y-1 text-xs">
<div className="flex items-center gap-2">
<span className="text-wing-muted w-12">IMO</span>
<span className="font-mono font-medium text-wing-text">{shipInfo.imoNo}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-wing-muted w-12">MMSI</span>
<span className="font-mono font-medium text-wing-text">{shipInfo.mmsiNo || '-'}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-wing-muted w-12">Status</span>
<span className="font-medium text-wing-text">{shipInfo.shipStatus || '-'}</span>
</div>
</div>
</div>
{/* 구분선 */}
<div className="w-px bg-wing-border" />
{/* 우측: 스펙 정보 */}
<div className="flex-1 grid grid-cols-2 gap-x-6 gap-y-1.5 text-xs">
<div className="flex items-center gap-2">
<span className="text-wing-muted w-16"></span>
<span className="font-medium text-wing-text">
{countryFlag(shipInfo.nationalityIsoCode)} {shipInfo.nationality || '-'}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-wing-muted w-16"></span>
<span className="font-medium text-wing-text">{shipInfo.shipType || '-'}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-wing-muted w-16">DWT</span>
<span className="font-medium text-wing-text">{shipInfo.dwt || '-'}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-wing-muted w-16">GT</span>
<span className="font-medium text-wing-text">{shipInfo.gt || '-'}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-wing-muted w-16"></span>
<span className="font-medium text-wing-text">{shipInfo.buildYear || '-'}</span>
</div>
</div>
</div>
)}
{companyInfo && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
<InfoField label="회사명" value={companyInfo.fullName} />
<InfoField label="회사코드" value={companyInfo.companyCode} />
<InfoField label="약칭" value={companyInfo.abbreviation} />
<InfoField label="국가" value={companyInfo.country} />
<InfoField label="도시" value={companyInfo.city} />
<InfoField label="등록국가" value={companyInfo.registrationCountry} />
<InfoField label="상태" value={companyInfo.status} />
<InfoField label="주소" value={companyInfo.address} />
<div className="flex gap-6">
{/* 좌측: 핵심 식별 정보 */}
<div className="min-w-[220px] space-y-2">
<div>
<div className="text-lg font-bold text-wing-text">{companyInfo.fullName || '-'}</div>
{companyInfo.abbreviation && (
<div className="text-xs text-wing-muted">{companyInfo.abbreviation}</div>
)}
</div>
<div className="space-y-1 text-xs">
<div className="flex items-center gap-2">
<span className="text-wing-muted w-16">Code</span>
<span className="font-mono font-medium text-wing-text">{companyInfo.companyCode}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-wing-muted w-16">Status</span>
<span className="font-medium text-wing-text">{companyInfo.status || '-'}</span>
</div>
{companyInfo.parentCompanyName && (
<div className="flex items-center gap-2">
<span className="text-wing-muted w-16"></span>
<span className="font-medium text-wing-text">{companyInfo.parentCompanyName}</span>
</div>
)}
</div>
</div>
{/* 구분선 */}
<div className="w-px bg-wing-border" />
{/* 우측: 상세 정보 */}
<div className="flex-1 grid grid-cols-2 gap-x-6 gap-y-1.5 text-xs">
<div className="flex items-center gap-2">
<span className="text-wing-muted w-16"></span>
<span className="font-medium text-wing-text">
{countryFlag(companyInfo.registrationCountryIsoCode)} {companyInfo.registrationCountry || '-'}
</span>
</div>
{companyInfo.controlCountry && (
<div className="flex items-center gap-2">
<span className="text-wing-muted w-16"></span>
<span className="font-medium text-wing-text">
{countryFlag(companyInfo.controlCountryIsoCode)} {companyInfo.controlCountry}
</span>
</div>
)}
{companyInfo.foundedDate && (
<div className="flex items-center gap-2">
<span className="text-wing-muted w-16"></span>
<span className="font-medium text-wing-text">{companyInfo.foundedDate}</span>
</div>
)}
{companyInfo.email && (
<div className="flex items-center gap-2">
<span className="text-wing-muted w-16"></span>
<span className="font-medium text-wing-text truncate">{companyInfo.email}</span>
</div>
)}
{companyInfo.phone && (
<div className="flex items-center gap-2">
<span className="text-wing-muted w-16"></span>
<span className="font-medium text-wing-text">{companyInfo.phone}</span>
</div>
)}
{companyInfo.website && (
<div className="flex items-center gap-2">
<span className="text-wing-muted w-16"></span>
<a
href={companyInfo.website.startsWith('http') ? companyInfo.website : `https://${companyInfo.website}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-blue-600 hover:underline truncate"
>
{companyInfo.website}
</a>
</div>
)}
</div>
</div>
)}
</div>

파일 보기

@ -10,9 +10,17 @@ public class CompanyInfoResponse {
private String companyCode;
private String fullName;
private String abbreviation;
private String country;
private String city;
private String status;
private String parentCompanyCode;
private String parentCompanyName;
private String registrationCountry;
private String address;
private String registrationCountryCode;
private String registrationCountryIsoCode;
private String controlCountry;
private String controlCountryCode;
private String controlCountryIsoCode;
private String foundedDate;
private String email;
private String phone;
private String website;
}

파일 보기

@ -10,6 +10,8 @@ public class ShipInfoResponse {
private String imoNo;
private String shipName;
private String shipStatus;
private String nationalityCode;
private String nationalityIsoCode;
private String nationality;
private String shipType;
private String dwt;

파일 보기

@ -34,4 +34,28 @@ public class CompanyDetailInfo {
@Column(name = "oa_addr", length = 512)
private String address;
@Column(name = "prnt_company_cd", length = 14)
private String parentCompanyCode;
@Column(name = "country_reg_cd", length = 6)
private String registrationCountryCode;
@Column(name = "country_ctrl", length = 40)
private String controlCountry;
@Column(name = "country_ctrl_cd", length = 6)
private String controlCountryCode;
@Column(name = "company_fndn_ymd", length = 8)
private String foundedDate;
@Column(name = "eml_addr", length = 320)
private String email;
@Column(name = "tel", length = 60)
private String phone;
@Column(name = "wbst_url", length = 120)
private String website;
}

파일 보기

@ -0,0 +1,25 @@
package com.snp.batch.global.model.screening;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "tb_ship_country_cd", schema = "std_snp_svc")
@Getter
@NoArgsConstructor
public class ShipCountryCode {
@Id
@Column(name = "ship_country_cd", length = 3)
private String shipCountryCode;
@Column(name = "cd_nm", length = 100)
private String codeName;
@Column(name = "iso_two_cd", length = 2)
private String isoTwoCode;
@Column(name = "iso_thr_cd", length = 3)
private String isoThreeCode;
}

파일 보기

@ -20,6 +20,9 @@ public class ShipInfoMaster {
@Column(name = "ship_status", length = 50)
private String shipStatus;
@Column(name = "ntnlty_cd", length = 50)
private String nationalityCode;
@Column(name = "ship_ntnlty", length = 57)
private String nationality;

파일 보기

@ -0,0 +1,12 @@
package com.snp.batch.global.repository.screening;
import com.snp.batch.global.model.screening.ShipCountryCode;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface ShipCountryCodeRepository extends JpaRepository<ShipCountryCode, String> {
Optional<ShipCountryCode> findByShipCountryCode(String shipCountryCode);
}

파일 보기

@ -10,6 +10,8 @@ import com.snp.batch.global.dto.screening.RiskCategoryResponse;
import com.snp.batch.global.dto.screening.RiskIndicatorResponse;
import com.snp.batch.global.dto.screening.ShipInfoResponse;
import com.snp.batch.global.model.screening.ChangeTypeLang;
import com.snp.batch.global.model.screening.CompanyDetailInfo;
import com.snp.batch.global.model.screening.ShipCountryCode;
import com.snp.batch.global.model.screening.CompanyComplianceHistory;
import com.snp.batch.global.model.screening.ComplianceIndicator;
import com.snp.batch.global.model.screening.ComplianceIndicatorLang;
@ -30,6 +32,7 @@ import com.snp.batch.global.repository.screening.RiskIndicatorCategoryLangReposi
import com.snp.batch.global.repository.screening.RiskIndicatorLangRepository;
import com.snp.batch.global.repository.screening.RiskIndicatorRepository;
import com.snp.batch.global.repository.screening.ShipComplianceHistoryRepository;
import com.snp.batch.global.repository.screening.ShipCountryCodeRepository;
import com.snp.batch.global.repository.screening.ShipInfoMasterRepository;
import com.snp.batch.global.repository.screening.ShipRiskDetailHistoryRepository;
import lombok.RequiredArgsConstructor;
@ -68,6 +71,7 @@ public class ScreeningGuideService {
private final CompanyComplianceHistoryRepository companyComplianceHistoryRepo;
private final ShipInfoMasterRepository shipInfoMasterRepo;
private final CompanyDetailInfoRepository companyDetailInfoRepo;
private final ShipCountryCodeRepository shipCountryCodeRepo;
private final JdbcTemplate jdbcTemplate;
/**
@ -275,6 +279,8 @@ public class ScreeningGuideService {
.imoNo(s.getImoNo())
.shipName(s.getShipName())
.shipStatus(s.getShipStatus())
.nationalityCode(s.getNationalityCode())
.nationalityIsoCode(resolveIsoTwoCode(s.getNationalityCode()))
.nationality(s.getNationality())
.shipType(s.getShipType())
.dwt(s.getDwt())
@ -293,16 +299,32 @@ public class ScreeningGuideService {
@Transactional(readOnly = true)
public CompanyInfoResponse getCompanyInfo(String companyCode) {
return companyDetailInfoRepo.findByCompanyCode(companyCode)
.map(c -> CompanyInfoResponse.builder()
.companyCode(c.getCompanyCode())
.fullName(c.getFullName())
.abbreviation(c.getAbbreviation())
.country(c.getCountry())
.city(c.getCity())
.status(c.getStatus())
.registrationCountry(c.getRegistrationCountry())
.address(c.getAddress())
.build())
.map(c -> {
String parentCompanyName = null;
if (c.getParentCompanyCode() != null && !c.getParentCompanyCode().isBlank()) {
parentCompanyName = companyDetailInfoRepo.findByCompanyCode(c.getParentCompanyCode())
.map(CompanyDetailInfo::getFullName)
.orElse("UNKNOWN");
}
return CompanyInfoResponse.builder()
.companyCode(c.getCompanyCode())
.fullName(c.getFullName())
.abbreviation(c.getAbbreviation())
.status(c.getStatus())
.parentCompanyCode(c.getParentCompanyCode())
.parentCompanyName(parentCompanyName)
.registrationCountry(c.getRegistrationCountry())
.registrationCountryCode(c.getRegistrationCountryCode())
.registrationCountryIsoCode(resolveIsoTwoCode(c.getRegistrationCountryCode()))
.controlCountry(c.getControlCountry())
.controlCountryCode(c.getControlCountryCode())
.controlCountryIsoCode(resolveIsoTwoCode(c.getControlCountryCode()))
.foundedDate(c.getFoundedDate())
.email(c.getEmail())
.phone(c.getPhone())
.website(c.getWebsite())
.build();
})
.orElse(null);
}
@ -408,6 +430,13 @@ public class ScreeningGuideService {
}
}
private String resolveIsoTwoCode(String shipCountryCode) {
if (shipCountryCode == null || shipCountryCode.isBlank()) return null;
return shipCountryCodeRepo.findByShipCountryCode(shipCountryCode)
.map(ShipCountryCode::getIsoTwoCode)
.orElse(null);
}
/**
* Risk 지표 컬럼명 필드명 매핑 조회
*/