release: 2026-03-31 (40건 커밋) #118
@ -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 지표 컬럼명 → 필드명 매핑 조회
|
||||
*/
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user