Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
226 changes: 226 additions & 0 deletions linkareer-data-mapping-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# 링커리어 데이터 → Picklab DB 적재 AI 변환 가이드

링커리어에서 크롤링한 비정형 CSV 데이터를 Picklab DB 스키마에 맞게 변환할 때 AI에게 전달하는 기준 지침이다.

---

## 입력 데이터 컬럼 (linkareer.csv)

| 컬럼명 | 설명 |
|--------|------|
| `/상세링크` | 링커리어 상세 링크 |
| `활동유형` | 대외활동 / 공모전/해커톤 / 교육 / 강연/세미나 |
| `출처` | 링커리어 |
| `제목` | 활동 제목 |
| `주최기관` | 기관명 → **`organizer` 컬럼에 그대로 저장** (기업형태 판단에도 활용) |
| `기업형태` | 링커리어의 원본 분류 값 |
| `참여대상` | 링커리어의 원본 참여 대상 |
| `접수기간` | 예: `2026.02.25 ~ 2026.03.08` |
| `활동기간` | 예: `26.3 ~ 26.10` |
| `모집인원` | 예: `10명`, `00명` (00명은 인원 미정으로 처리) |
| `모임지역` | 링커리어의 지역 분류 |
| `홈페이지` | 외부 링크 |
| `공모분야` | 공모전 도메인 분류 (링커리어 자체 분류) |
| `활동분야` | 대외활동 분야 분류 |
| `비용/시상규모` | 교육 비용 또는 공모전 시상 금액 |
| `썸네일이미지` | 썸네일 URL |
| `상세이미지` | 상세 이미지 URL (`;`로 복수 구분) |
| `상세내용` | 활동 상세 설명 텍스트 |

---

## 출력 필드별 변환 규칙

### 0. 주최기관명 (organizer) → `organizer` 컬럼

링커리어의 `주최기관` 값을 **그대로** `organizer` 컬럼(VARCHAR)에 저장한다. 변환 없음.

예시:
- `(사)한국이스포츠협회` → `organizer = "(사)한국이스포츠협회"`
- `방송콘텐츠진흥재단` → `organizer = "방송콘텐츠진흥재단"`

---

### 1. 활동유형 (ActivityType)

| 링커리어 값 | Picklab 값 |
|------------|-----------|
| 대외활동 | `대외활동` |
| 공모전/해커톤 | `공모전` |
| 교육 | `교육` |
| 강연/세미나 | `강연/세미나` |

---

### 2. 기업형태 (OrganizerType) → `organizer_type` 컬럼

링커리어의 `기업형태` 컬럼 값과 `주최기관` 기관명을 함께 보고 판단한다.

| 링커리어 값 | Picklab DB 값 (`organizer_type`) | 비고 |
|------------|----------------------------------|------|
| 대기업 | `LARGE_CORPORATION` | |
| 중견기업 | `MEDIUM_CORPORATION` | |
| 중소기업 | `SMALL_CORPORATION` | |
| 공공기관/공기업 | `PUBLIC_ORGANIZATION` | |
| 외국계기업 | `FOREIGN_CORPORATION` | |
| 스타트업 | `STARTUP` | |
| 금융권 | `FINANCIAL_INSTITUTION` | |
| 병원 | `HOSPITAL` | |
| 기타 | `ETC` | |
| 비영리단체/협회/재단 | **기관명으로 판단** | 아래 규칙 적용 |

**비영리단체/협회/재단 분리 규칙:**
- `주최기관` 이름에 **"재단"** 이 포함되면 → `NON_PROFIT` (재단 포함)
- 나머지는 → `NON_PROFIT`

> 현재 `OrganizerType`에 재단/협회 세분화가 없으므로 모두 `NON_PROFIT`으로 통일한다.

예시:
- `(사)한국이스포츠협회` → `NON_PROFIT`
- `(재)제천문화재단` → `NON_PROFIT`
- `방송콘텐츠진흥재단` → `NON_PROFIT`

---

### 3. 참여대상 (TargetAudience)

링커리어의 `참여대상` 컬럼을 아래 규칙으로 단일 값으로 변환한다.

| 링커리어 값 | Picklab 값 |
|------------|-----------|
| `대학생` | `대학생` |
| `직장인/일반인` | `직장인 및 일반인` |
| `대상 제한 없음` | `대상 제한 없음` |
| `청소년` 포함 | `대상 제한 없음` |
| `청소년, 대학생` 등 복수 | `대상 제한 없음` |
| `대학생, 직장인/일반인` 등 복수 | `대상 제한 없음` |
| 비어 있음 (null) | `null` 로 적재 |

**규칙 요약:** 참여 대상이 두 개 이상이거나 청소년이 포함된 경우, 대상 제한이 없는 것으로 간주하여 `대상 제한 없음`으로 설정한다.

---

### 4. 활동분야 (ActivityField) — 대외활동 한정

링커리어의 `활동분야` 컬럼을 Picklab 분류로 변환한다. `;`로 구분된 복수 값은 각각 매핑 후 대표 값 하나를 선택하거나 복수 태그로 처리한다.

| 링커리어 값 | Picklab 값 |
|------------|-----------|
| 서포터즈 | `서포터즈` |
| 마케터 | `마케터` |
| 멘토링 | `멘토링` |
| 기자단 | `기자단` |
| 봉사단-국내 | `국내봉사` |
| 해외봉사 | `해외봉사` |
| **해외탐방** | `해외봉사` (해외봉사와 동일하게 처리) |
| **강연** | `강연/세미나` |
| 기타 | `기타` |

---

### 5. 모임지역 (MeetingRegion)

| 링커리어 값 | Picklab 값 |
|------------|-----------|
| `지역 제한없음` | `지역 제한없음` |
| 서울 포함 (`서울 강남구`, `서울 종로구` 등) | `서울/인천` |
| 인천 포함 | `서울/인천` |
| 경기 포함 | `경기/강원` |
| 강원 포함 | `경기/강원` |
| 대전 포함 | `대전/세종/충남` |
| 충청 포함 | `대전/세종/충남` |
| 부산 포함 | `부산/대구/경상` |
| 대구 포함 | `부산/대구/경상` |
| 울산 포함 | `부산/대구/경상` |
| 경상 포함 | `부산/대구/경상` |
| 광주 포함 | `광주/전라` |
| 전라 포함 | `광주/전라` |
| 제주 포함 | `제주` |
| 해외 포함 | `해외` |
| 복수 지역이 포함된 경우 | `지역 제한없음` |

---

### 6. 도메인 (Domain) — 공모전 한정

링커리어의 `공모분야` 컬럼 값과 `제목`, `상세내용`을 종합적으로 분석하여 Picklab 도메인으로 분류한다.

**Picklab 도메인 목록:**
`SaaS`, `웹 (Web)`, `모바일 (App)`, `클라우드`, `AI`, `사물인터넷 (IoT)`, `교육`, `금융`, `게임 및 엔터테인먼트`, `생활과학 및 헬스케어`, `커머스`, `콘텐츠 및 소셜`, `교통 및 물류`, `AR 및 VR`, `전자기술 및 로보틱스`, `비즈니스 및 엔터프라이즈`, `기타`

**링커리어 공모분야 참고 매핑:**

| 링커리어 공모분야 | 우선 고려 Picklab 도메인 |
|-----------------|------------------------|
| 과학/공학 | `AI`, `전자기술 및 로보틱스`, `사물인터넷 (IoT)` 중 상세내용 보고 판단 |
| 기획/아이디어, 창업 | `비즈니스 및 엔터프라이즈` 또는 상세내용 보고 판단 |
| 사진/영상/UCC | `콘텐츠 및 소셜` |
| 문학/시나리오 | `콘텐츠 및 소셜` |
| 디자인/순수미술/공예 | `콘텐츠 및 소셜` |
| 캐릭터/만화/게임 | `게임 및 엔터테인먼트` |
| 네이밍/슬로건 | `콘텐츠 및 소셜` |
| 학술 | `교육` |
| 기타 | `기타` |

**중요:** 공모분야가 위 목록에 없거나 맞는 도메인이 불분명한 경우 반드시 `기타`로 설정한다. 억지로 매핑하지 않는다.

---

### 7. 시상규모 (PrizeRange) — 공모전 한정

링커리어의 `비용/시상규모` 컬럼에서 금액을 파싱하여 아래 기준으로 분류한다.

| 조건 | Picklab 값 |
|------|-----------|
| 시상 금액 합계 또는 최고 상금이 **500만 원 미만** | `5백만원 미만` |
| 시상 금액 합계 또는 최고 상금이 **500만 원 이상 ~ 5,000만 원 미만** | `5백만원 이상` |
| 시상 금액 합계 또는 최고 상금이 **5,000만 원 이상** | `5천만원 이상` |
| 금액 없음 (`-`, 비어 있음, 파싱 불가) | `null` 로 적재 |

금액 표기 예시 처리:
- `300만 원` → 300만 원 → `5백만원 미만`
- `1280만 원` → 1280만 원 → `5백만원 이상`
- `4000만 원` → 4000만 원 → `5백만원 이상`
- `1억 250만 원` → 1억 250만 원 → `5천만원 이상`
- `600억 원`, `800억 원` → `5천만원 이상`
- `-` → `null`

---

### 8. 교육 온/오프라인 (OnOffline) — 교육 한정

**적재 제외.** 교육 활동의 온/오프라인 필터는 데이터 충족률이 낮아 이번 적재에서 제외한다. 해당 필드는 `null`로 유지한다.

---

## 적재 제외 케이스

다음에 해당하는 row는 DB에 적재하지 않는다:

- `활동유형`이 위 매핑 표에 없는 경우
- `제목`이 비어 있는 경우

---

## 처리 예시

**입력:**
```
활동유형: 공모전/해커톤
제목: 2026 방콘진 혁신 방송 신기술 상용화 지원 공모 사업
주최기관: 방송콘텐츠진흥재단
기업형태: 비영리단체/협회/재단
참여대상: 대학생, 직장인/일반인
공모분야: 기획/아이디어; 과학/공학
비용/시상규모: 4000만 원
```

**출력:**
```
활동유형: COMPETITION
organizer: "방송콘텐츠진흥재단" ← 주최기관 그대로 저장
organizer_type: NON_PROFIT ← 기관명에 "재단" 포함이지만 enum은 NON_PROFIT 통일
참여대상: ALL ← 복수 대상 → ALL
도메인: BUSINESS_ENTERPRISE ← 기획/아이디어 기반, 상세내용 보고 판단
시상규모: 5백만원 이상 ← 4000만 원 → 500만~5000만 원 구간
```
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ fun ActivityCreateCommand.toEntity(activityGroup: ActivityGroup): Activity =
ExternalActivity(
title = title,
organizer = organizer,
organizerType = organizerType,
targetAudience = targetAudience,
location = location,
recruitmentStartDate = recruitmentStartDate,
Expand All @@ -44,6 +45,7 @@ fun ActivityCreateCommand.toEntity(activityGroup: ActivityGroup): Activity =
SeminarActivity(
title = title,
organizer = organizer,
organizerType = organizerType,
targetAudience = targetAudience,
location = location,
recruitmentStartDate = recruitmentStartDate,
Expand All @@ -66,6 +68,7 @@ fun ActivityCreateCommand.toEntity(activityGroup: ActivityGroup): Activity =
EducationActivity(
title = title,
organizer = organizer,
organizerType = organizerType,
targetAudience = targetAudience,
location = location,
recruitmentStartDate = recruitmentStartDate,
Expand All @@ -91,6 +94,7 @@ fun ActivityCreateCommand.toEntity(activityGroup: ActivityGroup): Activity =
CompetitionActivity(
title = title,
organizer = organizer,
organizerType = organizerType,
targetAudience = targetAudience,
recruitmentStartDate = recruitmentStartDate,
recruitmentEndDate = recruitmentEndDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ fun ActivityView.withBookmark(isBookmarked: Boolean): ActivityItemWithBookmark {
id,
title,
organization,
organizerType,
startDate,
category,
jobTags,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ sealed class ActivityCreateCommand(
open val activityType: ActivityType,
open val activityGroupId: Long,
open val title: String,
open val organizer: OrganizerType,
open val organizer: String?,
open val organizerType: OrganizerType,
open val targetAudience: ParticipantType,
open val recruitmentStartDate: LocalDate,
open val recruitmentEndDate: LocalDate?,
Expand All @@ -37,7 +38,8 @@ sealed class ActivityCreateCommand(
data class ExternalActivityCreateCommand(
override val activityGroupId: Long,
override val title: String,
override val organizer: OrganizerType,
override val organizer: String? = null,
override val organizerType: OrganizerType,
override val targetAudience: ParticipantType,
override val recruitmentStartDate: LocalDate,
override val recruitmentEndDate: LocalDate?,
Expand All @@ -60,6 +62,7 @@ data class ExternalActivityCreateCommand(
activityGroupId = activityGroupId,
title = title,
organizer = organizer,
organizerType = organizerType,
targetAudience = targetAudience,
recruitmentStartDate = recruitmentStartDate,
recruitmentEndDate = recruitmentEndDate,
Expand All @@ -80,7 +83,8 @@ data class ExternalActivityCreateCommand(
data class SeminarActivityCreateCommand(
override val activityGroupId: Long,
override val title: String,
override val organizer: OrganizerType,
override val organizer: String? = null,
override val organizerType: OrganizerType,
override val targetAudience: ParticipantType,
override val recruitmentStartDate: LocalDate,
override val recruitmentEndDate: LocalDate?,
Expand All @@ -102,6 +106,7 @@ data class SeminarActivityCreateCommand(
activityGroupId = activityGroupId,
title = title,
organizer = organizer,
organizerType = organizerType,
targetAudience = targetAudience,
recruitmentStartDate = recruitmentStartDate,
recruitmentEndDate = recruitmentEndDate,
Expand All @@ -122,7 +127,8 @@ data class SeminarActivityCreateCommand(
data class EducationActivityCreateCommand(
override val activityGroupId: Long,
override val title: String,
override val organizer: OrganizerType,
override val organizer: String? = null,
override val organizerType: OrganizerType,
override val targetAudience: ParticipantType,
override val recruitmentStartDate: LocalDate,
override val recruitmentEndDate: LocalDate?,
Expand All @@ -147,6 +153,7 @@ data class EducationActivityCreateCommand(
activityGroupId = activityGroupId,
title = title,
organizer = organizer,
organizerType = organizerType,
targetAudience = targetAudience,
recruitmentStartDate = recruitmentStartDate,
recruitmentEndDate = recruitmentEndDate,
Expand All @@ -167,7 +174,8 @@ data class EducationActivityCreateCommand(
data class CompetitionActivityCreateCommand(
override val activityGroupId: Long,
override val title: String,
override val organizer: OrganizerType,
override val organizer: String? = null,
override val organizerType: OrganizerType,
override val targetAudience: ParticipantType,
override val recruitmentStartDate: LocalDate,
override val recruitmentEndDate: LocalDate?,
Expand All @@ -190,6 +198,7 @@ data class CompetitionActivityCreateCommand(
activityGroupId = activityGroupId,
title = title,
organizer = organizer,
organizerType = organizerType,
targetAudience = targetAudience,
recruitmentStartDate = recruitmentStartDate,
recruitmentEndDate = recruitmentEndDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import java.time.LocalDate
data class ActivityItemWithBookmark(
val id: Long,
val title: String,
val organization: String,
val organization: String?,
val organizerType: String,
val startDate: LocalDate,
val category: String,
val jobTags: List<String>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import picklab.backend.job.domain.enums.JobDetail
data class ActivitySearchCondition(
val category: ActivityType,
val jobTag: List<JobDetail>?,
val organizer: List<OrganizerType>?,
val organizerType: List<OrganizerType>?,
val target: List<ParticipantType>?,
val field: List<ActivityFieldType>?,
val location: List<LocationType>?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import java.time.LocalDate
interface ActivityView {
val id: Long
val title: String
val organization: String
val organization: String?
val organizerType: String
val startDate: LocalDate
val category: String
val jobTags: List<String>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,13 @@ abstract class Activity(
@Column(name = "title", nullable = false, length = 50)
@Comment("활동명")
var title: String,
@Column(name = "organizer", nullable = false, length = 50)
@Enumerated(EnumType.STRING)
@Column(name = "organizer")
@Comment("주최 기관/단체명")
var organizer: OrganizerType,
var organizer: String? = null,
@Column(name = "organizer_type", nullable = false, length = 50)
@Enumerated(EnumType.STRING)
@Comment("주최 기관 유형")
var organizerType: OrganizerType,
@Column(name = "target_audience", nullable = false, length = 50)
@Enumerated(EnumType.STRING)
@Comment("참여대상")
Expand Down
Loading
Loading