본 스킬은 대한민국 공공데이터포털(https://www.data.go.kr/)에서 제공하는 각종 공공데이터 API 를 사용하기 위한 설명입니다. 본 스킬은 공공데이터 개발 또는 공공 API 개발을 할 때 사용하면 됩니다.
⚠️ Center 프로젝트에서 글 관련 작업(생성, 수정, 삭제, 배치 생성 등)을 수행할 때는 반드시 다음 순서로 문서를 확인하세요:
- api-spec.md - API 사용 가이드, 테스트용 토큰, curl 예시
- create_post.sh - 단일/배치 게시글 생성 스크립트
**대한민국 공공데이터포털(data.go.kr)**은 중앙정부, 지자체, 공공기관이 보유한 데이터를 개방·제공하는 공식 플랫폼이다.
중요: 공공데이터포털 API 문서와 실제 응답 구조가 다를 수 있다. 아래는 실제 응답 구조이다.
{
"response": {
"header": {
"resultCode": "0",
"resultMsg": "정상"
},
"body": {
"dataType": "JSON",
"items": {
"item": [
{ /* 실제 데이터 항목 */ }
]
},
"numOfRows": 10,
"pageNo": 1,
"totalCount": 225
}
}
}
| 항목 | 문서 (잘못됨) | 실제 응답 (올바름) |
|---|---|---|
| 결과 코드 | resultCode (int) | response.header.resultCode (string) |
| 데이터 배열 | data[] | response.body.items.item[] |
| 총 개수 | totalCount | response.body.totalCount |
| 코드 | 메시지 | 설명 |
|---|---|---|
0 | OK | 정상 |
-1 | 시스템 내부 오류 | 서버 에러 |
-2 | 파라미터 부적합 | 요청 파라미터 오류 |
-4 | 등록되지 않은 인증키 | 인증키 미등록 |
-10 | 트래픽 초과 | 일일 호출 횟수 초과 |
-401 | 유효하지 않은 인증키 | 인증키 오류 |
scripts/test-mofa-api.sh)외교부 공지사항 API를 터미널에서 직접 테스트할 수 있는 쉘 스크립트이다.
# 스크립트 실행
./scripts/test-mofa-api.sh "<URL_ENCODED_API_KEY>"
# 예시
./scripts/test-mofa-api.sh "FAK7%2BJL3rqrFr7Wtn%2FxkKhW8hq1zDsite%2FxQdIwug4pDLD5bsqFJDKzroRXTkY8fm5LXMMMzIaTuvl%2F4iDtQ%2Bw%3D%3D"
chmod +x scripts/test-mofa-api.sh| API | 문서 | 설명 |
|---|---|---|
| 외교부 공지사항 | references/mofa-reminder.md | 출국 전 참고 공지사항 목록 조회 |
http://apis.data.go.kr/1262000/NoticeService2/getNoticeList2Flutter 앱에서 공공데이터 API를 사용하는 방법이다. 본 프로젝트의 lib/services/data/ 디렉토리에 구현되어 있다.
lib/services/data/
├── data.service.dart # API 호출 및 캐싱 서비스 (싱글톤)
└── mofa_notice.model.dart # 응답 데이터 모델
mofa_notice.model.dart)class MofaNotice {
final String id; // 공지사항 ID (예: "ATC0000000048200")
final String title; // 제목 (HTML 엔티티 포함)
final String content; // 내용 (txt_origin_cn)
final String writtenDate; // 작성일 (예: "2025-12-15")
final String fileUrl; // 첨부파일 URL (없으면 빈 문자열)
// HTML 엔티티 디코딩된 제목/내용 반환
String get decodedTitle => _decodeHtmlEntities(title);
String get decodedContent => _decodeHtmlEntities(content);
}
class MofaNoticeResponse {
final String resultCode; // "0" = 성공
final String resultMsg; // 결과 메시지
final int totalCount; // 전체 공지사항 수
final int numOfRows; // 요청한 개수
final int pageNo; // 페이지 번호
final List<MofaNotice> notices; // 공지사항 목록
final DateTime fetchedAt; // 조회 시간
bool get isSuccess => resultCode == '0';
// API 응답 파싱 (실제 구조에 맞게)
factory MofaNoticeResponse.fromApiJson(Map<String, dynamic> json) {
final response = json['response'];
final header = response['header'];
final body = response['body'];
final items = body['items']['item'] as List;
// ...
}
}
data.service.dart)싱글톤 패턴으로 구현된 API 서비스이다.
import 'package:philgo/services/data/data.service.dart';
// 1. 공지사항 로드 (캐시 우선)
final response = await DataService.instance.loadMofaNotices();
// 2. 성공 여부 확인
if (response.isSuccess) {
// 3. 공지사항 목록 접근
for (final notice in response.notices) {
print('제목: ${notice.decodedTitle}');
print('날짜: ${notice.writtenDate}');
print('내용: ${notice.decodedContent}');
}
}
// 4. 캐시 강제 초기화 (새로고침)
await DataService.instance.clearMofaCache();
// 5. 캐시 남은 시간 확인
final remaining = DataService.instance.mofaCacheRemainingTime;
class DataService {
static DataService? _instance;
static DataService get instance => _instance ??= DataService._();
// API Key (이미 URL 인코딩됨)
static const String apiKey = AppConfig.dataApiKey;
// 1시간 캐시 TTL
static const Duration _mofaCacheTtl = Duration(hours: 1);
// FileCache 사용 (메모리 + 파일 이중 캐싱)
late final FileCache<MofaNoticeResponse> _mofaCache = FileCache(
cacheName: 'mofa_notices',
defaultTtl: _mofaCacheTtl,
fromJson: MofaNoticeResponse.fromJson,
toJson: (data) => data.toJson(),
useMemoryCache: true,
);
Future<MofaNoticeResponse> loadMofaNotices() async {
// 1. 캐시 확인
final cached = await _mofaCache.get('mofa_notices');
if (cached != null) return cached;
// 2. API 호출
final response = await http.get(Uri.parse(
'$_mofaApiBaseUrl?serviceKey=$apiKey&returnType=JSON&numOfRows=5&pageNo=1'
));
// 3. UTF-8 디코딩 (한글 깨짐 방지)
final json = jsonDecode(utf8.decode(response.bodyBytes));
// 4. 실제 응답 구조에 맞게 파싱
final data = MofaNoticeResponse.fromApiJson(json);
// 5. 캐시 저장
await _mofaCache.set('mofa_notices', data);
return data;
}
}
class _NoticeScreenState extends State<NoticeScreen> {
bool _isLoading = true;
MofaNoticeResponse? _response;
@override
void initState() {
super.initState();
_loadNotices();
}
Future<void> _loadNotices() async {
final response = await DataService.instance.loadMofaNotices();
setState(() {
_response = response;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return CircularProgressIndicator();
}
if (!_response!.isSuccess) {
return Text('에러: ${_response!.resultMsg}');
}
return ListView.builder(
itemCount: _response!.notices.length,
itemBuilder: (context, index) {
final notice = _response!.notices[index];
return ListTile(
title: Text(notice.decodedTitle),
subtitle: Text(notice.writtenDate),
onTap: () => _showDetail(notice),
);
},
);
}
}
utf8.decode(response.bodyBytes) 사용', 등이 포함될 수 있음