본문 바로가기

제휴문의

전국교회연동을 통한 목회자 청빙 게시판 시스템 로드맵

전국교회연동을 통한 목회자 청빙 게시판 시스템 로드맵

기술 스택 및 아키텍처 개요

  • Next.js 기반 프레임워크: 프론트엔드 UI와 백엔드 API 서버 기능을 모두 갖춘 Next.js를 사용합니다. React를 통한 SSR/CSR을 지원하며, 페이지 라우팅과 API 경로(pages/api/*)를 활용해 백엔드 로직 구현이 가능합니다.
  • PHP 전환 고려: 초기에는 Next.js로 개발하지만, 추후 PHP로의 전환을 대비하여 API 중심 설계를 채택합니다. 모든 핵심 기능을 RESTful API로 구성하여, 필요한 경우 동일한 API 레이어를 PHP로 재구현하거나, PHP 기반 프론트엔드가 이 API를 호출하도록 합니다. 이를 위해 프론트엔드와 백엔드 로직을 분리하고, Next.js의 API Routes를 이용한 서버사이드 로직은 나중에 PHP (예: Laravel 등의 프레임워크)로 이식하기 쉽도록 표준 HTTP API 형태로 만듭니다.
  • 데이터베이스: 관계형 DB (예: MySQL 또는 PostgreSQL)를 사용하여 교회 정보, 회원 정보, 게시판 글 등을 저장합니다. Next.js 환경에서는 Prisma ORM 또는 Sequelize 등을 통해 DB를 조작하고, PHP 환경 전환 시 동일 DB에 접근하거나 이식할 수 있게 합니다. 공통 DB 스키마를 정의하여 Node와 PHP에서 모두 활용할 수 있도록 합니다.
  • 전체 구성: Next.js 애플리케이션 (React UI + Node.js API) → DB 레이어(MySQL 등) → 외부 연동(API, OAuth)로 이루어진 계층적 구조입니다. Next.js는 클라이언트 요청에 대해 API 레이어를 거쳐 DB나 외부 API를 사용하고, UI 페이지는 이 API 레이어를 통해 데이터를 가져옵니다. 이러한 구조는 PHP로 백엔드를 변경해도 프론트엔드 (혹은 다른 서비스)에서 동일한 API 규약으로 통신할 수 있게 해 줍니다.

공공데이터 연계: 전국 교회 정보 수집

  • 공공데이터포털 API 활용: 대한민국 공공데이터포털(data.go.kr)의 종교시설 현황 OpenAPI를 이용하여 전국 교회 정보를 수집합니다. 각 지방자치단체 또는 기관에서 제공하는 "종교시설" 데이터셋의 OpenAPI가 존재하며, 예를 들어 충청남도 계룡시의 경우 relgnFcltySttusService API를 통해 종교, 시설명, 도로명주소, 우편번호, 전화번호 등의 정보를 조회할 수 있습니다. 전국 교회 데이터를 위해서는 각 지역(시군구)의 종교시설 API를 순차적으로 호출하거나, 공공데이터포털에 제공된 파일 데이터를 활용하여 전국 데이터를 통합해야 합니다.
  • API 인증 및 호출: 공공데이터포털에서 발급된 API 키(serviceKey)를 사용하여 REST API를 호출합니다. 요청 시 **페이지번호(currentPage)**와 **페이지당 건수(perPage)**를 지정하여 데이터를 페이지네이션 방식으로 모두 수집합니다. 또한 종교 필터(RELGN) 파라미터를 "기독교"로 지정하여 교회(기독교) 데이터만 조회합니다. (예: GET http://apis.data.go.kr/.../getrelgnFcltySttus?serviceKey=...&RELGN=기독교&perPage=100¤tPage=1)
  • 데이터 통합 및 업데이트: 수집된 교회 정보를 지역 구분과 함께 하나의 교회 정보 DB 테이블에 저장합니다. 지역별 API에는 고유 식별자가 없을 수 있으므로, 지역 + 시설명 + 주소 등을 복합 키로 중복을 방지합니다. 정기적으로(예: 매월 또는 데이터 제공처 업데이트 주기에 따라) 스케줄러(크론 잡 등)를 통해 API를 호출, 신규/변경사항을 반영합니다. 기존 데이터를 업데이트할 때는 시설명이나 주소가 변경된 경우 해당 레코드를 갱신하고, 신규 교회는 삽입합니다. 불필요한 중복 호출을 피하고 성능을 위해, API 결과를 캐싱하거나 파일 데이터를 주기적으로 내려받아 일괄 업데이트하는 방법도 고려합니다.
  • DB 저장 필드: 공공데이터 응답의 필드에 맞춰 DB 컬럼을 설계합니다. 예를 들어 Churches 테이블을 만들어 id (PK), name(교회명), address(도로명주소), zip(우편번호), tel(전화번호), religion(종교 유형), region_code(지역 식별자) 등의 컬럼을 둡니다. (아래 DB 스키마 예시 참고) API 응답이 JSON 형태이므로, Next.js 서버에서 이를 파싱하여 각 필드에 매핑합니다. 예시: 응답 JSON에서 FCLTY_NM → name, RDNMADR → address로 매핑 후 DB 저장.

회원가입 및 SNS 로그인 기능

  • 일반 회원가입: 사용자가 이메일/비밀번호로 가입할 수 있는 로컬 회원가입을 지원합니다. 이를 위해 회원(User) 테이블을 설계하고, 이메일(unique), 비밀번호 해시, 이름/별명 등의 필드를 가집니다. 비밀번호는 Bcrypt 등으로 해시하여 저장하고, 가입시 이메일 인증 절차를 둘 수 있습니다 (필요시). Next.js에서는 API Route (pages/api/register.js 등)를 만들어 회원가입 요청(POST)을 처리하고 DB에 사용자 정보를 저장합니다.
  • SNS 로그인(OAuth): 카카오, 네이버, 구글 등의 소셜 로그인을 지원하여 사용자가 편리하게 로그인하도록 합니다. Next.js 환경에서는 NextAuth 라이브러리를 사용하면 여러 OAuth 공급자를 손쉽게 통합할 수 있습니다. 예를 들어 Kakao, Naver, Google 각각에 앱을 등록한 후 클라이언트 ID/시크릿을 얻어 NextAuth 프로바이더로 설정합니다. 예시: NextAuth 설정파일에서 Kakao Provider를 추가하면, 몇 줄의 설정만으로 Kakao 로그인 연동이 가능합니다. 아래는 NextAuth 설정 예시입니다:
// File: pages/api/auth/[...nextauth].js - NextAuth 설정 (Kakao/Naver/Google 로그인 포함)
import NextAuth from "next-auth";
import KakaoProvider from "next-auth/providers/kakao";
import NaverProvider from "next-auth/providers/naver";
import GoogleProvider from "next-auth/providers/google";

export default NextAuth({
  providers: [
    // Kakao 소셜 로그인 설정 (Kakao OAuth)
    KakaoProvider({
      clientId: process.env.KAKAO_CLIENT_ID,       // Kakao 앱의 REST API 키
      clientSecret: process.env.KAKAO_CLIENT_SECRET // Kakao 앱의 시크릿 키 (필요시)
    }),
    // Naver 소셜 로그인 설정 (Naver OAuth)
    NaverProvider({
      clientId: process.env.NAVER_CLIENT_ID,       // 네이버 앱의 Client ID
      clientSecret: process.env.NAVER_CLIENT_SECRET // 네이버 앱 Client Secret
    }),
    // Google 소셜 로그인 설정 (Google OAuth 2.0)
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,      // 구글 OAuth 클라이언트 ID
      clientSecret: process.env.GOOGLE_CLIENT_SECRET // 구글 OAuth 클라이언트 시크릿
    })
  ],
  // 추가 설정: 데이터베이스 연결 또는 JWT 사용 등
  // adapter: ... (예: Prisma Adapter를 사용하여 DB에 사용자 저장)
});

주석: 위 설정으로 카카오/네이버/구글 로그인 API가 활성화되며, 사용자는 해당 서비스 인증 후 우리 사이트에 로그인됩니다. NextAuth를 사용하면 세션 관리, 쿠키 처리 등이 자동화되고, OAuth 응답으로 받은 사용자 정보로 우리 서비스의 사용자 계정을 생성하거나 연결합니다. 예컨대 처음 소셜 로그인 시 User 테이블에 새 레코드를 만들고 이후 로그인 시 해당 사용자와 매핑합니다. PHP 환경 전환 시에는 OAuth2 Client 라이브러리들을 활용하여 유사한 흐름을 구현할 수 있습니다 (예: PHP용 League OAuth2 client 등).

  • 세션 관리: Next.js/NextAuth 기반 로그인은 JWT나 세션 쿠키를 통해 유지됩니다. NextAuth 기본 설정으로 JWT를 사용할 경우 API Route에서 getServerSession 등으로 현재 로그인 정보를 받아 활용할 수 있고, 프론트엔드에서는 useSession() 훅으로 로그인 상태를 가져옵니다. (PHP로 전환할 경우 PHP 세션이나 JWT 기반 인증으로 전환)
  • 접근 제어: 청빙 게시판 글쓰기수정 등 기능은 로그인된 사용자만 가능하도록 미들웨어에서 세션을 검증합니다. 또한 자신의 글만 수정/삭제 가능하며, 관리자 계정은 전체 글을 관리할 수 있도록 권한 체계를 둡니다.

청빙 게시판 기능 및 A/B 사이트 간 연동

  • 게시판 기본 기능: 담임목사 청빙과 같은 공고를 올릴 수 있는 게시판을 구현합니다. 게시글에는 제목, 내용, 교회명(연결된 교회 정보), 작성자, 작성일 등 필드를 포함합니다. 교회명은 수기 입력 대신 앞서 수집한 교회 정보 DB와 연동하여 선택하도록 합니다. (예: 글 작성 폼에서 교회를 검색/선택하면 해당 교회 ID를 글에 연계) 이를 통해 게시글에 정확한 교회 정보(주소, 교단 등 추가정보 활용 가능)를 연결하고 검색에도 활용할 수 있습니다. 사용자는 게시판 리스트에서 제목, 교회명, 날짜를 확인하고, 클릭 시 상세 내용을 보도록 UI/라우팅을 구성합니다.
  • 게시글 작성/수정: Next.js API Route를 통해 POST /api/posts 요청을 처리하여 새로운 청빙 공고를 DB에 저장합니다. 게시글 저장 시 현재 로그인된 사용자의 ID를 연계하여 작성자를 기록하고, 생성 시각, 고유 ID 등을 부여합니다. 수정의 경우 PUT /api/posts/[id] 요청으로 처리하며, 서버에서 권한 검증(작성자 또는 관리자 여부) 후 내용 및 교회 정보를 업데이트합니다. 삭제(필요시)도 비슷한 방식으로 구현합니다.
  • A사이트-B사이트 연동 배경: 동일한 청빙 공고를 두 개의 사이트(A와 B)에 동시에 게시하고 동기화하려는 요구사항이 있습니다. 이를 위해 양측 사이트가 서로 API를 통해 소통하여 게시글을 공유하도록 설계합니다. 예를 들어, A사이트에서 글이 작성되면 B사이트에도 자동 등록되고, A에서 수정하면 B에도 수정사항이 반영되도록 합니다.
  • 동기화 아키텍처: 중앙 집약형 vs 분산형 두 가지 접근이 가능합니다. (1) 중앙 집약형: A와 B가 모두 공용으로 사용하는 중앙 DB 또는 중앙 API 서비스를 두어 한쪽에서 글 작성 시 실제 데이터는 중앙에 저장되고, 다른 쪽은 중앙 데이터를 조회만 하는 구조입니다. (2) 분산형(양방향 동기화): A와 B가 각자 독립적인 DB를 가지지만, 한쪽에서 생성/수정된 내용을 다른 쪽에 REST API 호출을 통해 전송하여 동기화하는 구조입니다.
    • 여기서는 API 기반 분산형 동기화를 중점적으로 설명합니다. 이 방식에서는 A사이트에서 게시글 생성 시 B사이트의 Open API (예: B의 /api/posts 엔드포인트)를 호출하여 동일한 내용을 전달하고 저장시킵니다. 반대로 B에서 수정이 발생하면 A의 API를 호출합니다.
    • 양방향 동기화 세부구현: 각 사이트는 상대방이 호출할 수 있는 전용 API 엔드포인트인증 수단을 가집니다. 예를 들어 A사이트에 /api/posts/receive (POST) API를 만들고, B사이트에도 /api/posts/receive API를 만들어 놓습니다. 글 작성 또는 수정 시, 발신 측 사이트에서는 수신 측의 /receive API를 호출하면서, 게시글의 고유 식별자(ID 또는 UUID), 제목, 내용, 교회ID 등 필요한 데이터를 JSON으로 전송합니다. 이 호출에는 사전에 양 사이트가 공유한 **보안 토큰(API Key 또는 JWT)**을 포함하여 인증된 요청만 처리되도록 합니다. 수신 측 사이트는 해당 요청을 받아 DB에 변경사항을 적용합니다.
    • 동일 ID 및 중복 방지: 처음 글이 생성될 때 양쪽에 서로 다른 PK가 생길 수 있으므로, 공유 가능한 식별자를 추가로 사용합니다. 예를 들어 UUID를 생성하여 게시글의 global_id 필드로 저장하고, 동기화 시 이 값을 전달하여 B사이트도 이 값을 함께 저장합니다. 이후 수정/동기화 시에는 이 global_id를 키로 상대 사이트 데이터를 조회/수정하면 서로 동일한 게시글을 가리킬 수 있습니다. (또는 한쪽 사이트의 ID를 다른 쪽에 저장하는 매핑 테이블을 둘 수도 있습니다.)
    • 실시간 동기화 흐름: A에서 POST 발생 -> A서버가 DB 저장 -> 성공 후 B의 API 호출 (본문에 글 데이터 payload) -> B서버에서 수신하여 DB저장 -> 응답 확인. 업데이트도 유사하게 A에서 수정 -> B에 수정 API 호출. 만약 B사이트가 응답 실패하면 재시도 로직이나 관리자 알림을 통해 일관성을 확보합니다. 또한 양쪽 모두 최신 데이터 동기화 검증을 위해 주기적으로 global_id 기반으로 데이터를 대조하는 관리 기능을 고려할 수 있습니다.
  • 게시판 UI/UX: A와 B 두 사이트의 게시판 UI는 동일한 데이터를 표시하므로, UI 구성은 유사하게 가져갑니다. Next.js에서는 동적 라우트 (예: /posts/[id].jsx)로 게시글 페이지를 만들고 SSR로 해당 ID의 글을 불러오도록 구현할 수 있습니다. 게시글 목록은 /posts 페이지에서 API를 통해 데이터 페칭하여 렌더링합니다.

전체 시스템 기능 구성도 및 데이터 흐름

  • 기능 개요: 본 시스템은 교회 정보 관리, 회원 인증, 청빙 게시판, 사이트간 동기화 네 가지 핵심 모듈로 구성됩니다. 아래는 각 모듈별 기능 요소와 데이터 흐음을 요약한 로드맵입니다.
    1. 교회 정보 모듈: 공공데이터포털의 교회 데이터를 주기적으로 가져와 DB에 저장하고, 게시글 작성 시 교회선택 등에 활용.
      • 데이터 흐름: 공공데이터 OpenAPI → (정기수집) → Next.js API (fetchChurchData) → DB (Churches 테이블 저장) → 프론트엔드 (교회 목록 조회).
    2. 회원 인증 모듈: 로컬 회원가입/로그인 및 SNS(OAuth) 로그인을 처리하며, 사용자 세션을 관리.
      • 데이터 흐름: 사용자 입력 또는 SNS OAuth 콜백 → Next.js Auth API (NextAuth 등의 인증 처리) → DB (Users 테이블 저장/조회) → 세션 쿠키/JWT → 프론트엔드 접근 제어.
    3. 청빙 게시판 모듈: 사용자가 청빙 공고를 CRUD 할 수 있는 게시판 기능과 UI 제공.
      • 데이터 흐름: 프론트엔드에서 게시글 작성 요청 → Next.js API (/api/posts) → DB (Posts 테이블 insert/update) → 응답 → 프론트엔드 갱신. 게시글 조회는 프론트엔드 요청 → Next.js API (/api/posts[?query] 또는 SSR) → DB 조회 → 데이터 반환.
    4. 사이트간 동기화 모듈: A↔B 사이트 간 게시글 동기화를 위한 전용 API 통신.
      • 데이터 흐름: 한쪽 사이트에서 게시글 생성/수정 → 해당 서버가 상대 사이트 API 호출(JSON payload 전송) → 수신 서버에서 DB 업데이트 → 양측 데이터 동기화 유지.
  • API 엔드포인트 설계: 기능에 따라 다음과 같은 RESTful API 엔드포인트들을 설계합니다. (Next.js에서는 /api 아래에 구현되고, PHP 전환 시 동일한 경로로 구현 권장)
    • 교회 데이터:
      • GET /api/churches?search= – 교회 목록 조회 (필요시 이름 검색 가능)
      • POST /api/churches/update – (관리자용 또는 내부용) 공공데이터 API로부터 교회데이터 업데이트 트리거. (크론잡 대용으로 수동 갱신)
    • 인증:
      • POST /api/register – 회원가입 처리 (이메일, 비밀번호 등 수신)
      • POST /api/login – 로그인 처리 (로컬 로그인용; SNS 로그인의 경우 OAuth 루트 사용)
      • GET /api/auth/session – 현재 세션 정보 (NextAuth 사용 시 기본 제공)
      • /api/auth/* – SNS OAuth 콜백 및 세션 유지를 위한 NextAuth 엔드포인트들 (NextAuth 프레임워크 활용)
    • 게시판:
      • GET /api/posts – 게시글 목록 조회 (페이지네이션, 교회명/지역별 필터 등 쿼리 파라미터)
      • GET /api/posts/[id] – 특정 게시글 상세 조회
      • POST /api/posts – 새 게시글 생성 (JSON 본문에 title, content, churchId 등)
      • PUT /api/posts/[id] – 게시글 수정
      • DELETE /api/posts/[id] – 게시글 삭제 (필요시)
    • 동기화:
      • POST /api/posts/receive – 다른 사이트로부터 게시글 생성/수정 내용 수신. (인증 필요, JSON payload에 게시글 데이터 및 global_id 포함)
      • POST /api/posts/sync/[id] – (선택사항) 특정 게시글을 수동 동기화 또는 재동기화 요청.
      • 참고: 동기화는 생성/수정 API 내에서 상대 사이트 호출로 이루어지므로, 별도 공개 엔드포인트를 최소화하고자 한다면 receive 하나만 공개하고 나머지는 내부 로직으로 처리 가능합니다.
  • 데이터 흐름 상세:
    • 교회 데이터 업데이트: 관리 스크립트 또는 cron이 /api/churches/update를 호출 → Next.js API가 각 지역 OpenAPI 반복 호출 → 응답 JSON 파싱 후 DB upsert → 로깅.
    • 게시글 생성: 사용자 입력 → /api/posts 호출 (토큰 포함) → 서버에서 DB 저장 (Posts 레코드 생성, ID 할당, global_id 생성) → 저장 완료 후 동기화 로직에서 fetch()로 B사이트 /api/posts/receive 호출 (payload: 글 데이터) → B사이트에서 인증 확인 후 DB에 해당 글 저장 (또는 업데이트). → 두 사이트 모두 사용자에게 성공 응답.
    • 게시글 조회: 사용자가 게시판 페이지 접속 → Next.js가 SSR 또는 CSR로 /api/posts 목록 요청 → DB 조회 결과 반환 → 화면에 렌더링. 상세페이지는 /api/posts/[id]로 해당 글과 관련 교회 정보를 조인하여 반환, 화면 표시. 양 사이트 모두 같은 global_id의 게시글은 내용이 동일하므로 사용자 경험에는 차이가 없습니다.
  • 에러 처리 및 로그: 각 모듈에 대한 에러 상황을 고려합니다. 예를 들어 공공데이터 API 호출 실패 시 재시도 또는 관리자 경고, 동기화 실패 시 큐잉/재전송, 권한 없는 사용자의 요청은 401/403 응답 등. 이러한 로그는 DB 또는 로그관리시스템에 기록하여 추적합니다.

데이터베이스 설계 (스키마 예시)

시스템의 핵심 데이터 엔터티에 대한 DB 테이블 스키마 예시는 다음과 같습니다 (MySQL 기준). 실제 개발 시 ORM 모델 정의로 구현할 수 있습니다.

-- Churches: 전국 교회 정보 테이블
CREATE TABLE Churches (
  church_id     INT PRIMARY KEY AUTO_INCREMENT,
  name          VARCHAR(100) NOT NULL,    -- 교회명 (예: 나눔의교회)
  denomination  VARCHAR(50) NULL,         -- 교단/종파 (옵션, API에 없으면 추후 수동 입력 가능)
  address       VARCHAR(200) NOT NULL,    -- 도로명 주소
  region        VARCHAR(50) NOT NULL,     -- 지역 (예: 충남 계룡시)
  zip_code      VARCHAR(10) NOT NULL,     -- 우편번호
  phone         VARCHAR(20) NULL,         -- 전화번호 (없을 수 있어 NULL 허용)
  data_source   VARCHAR(50) NOT NULL,     -- 데이터 출처 (예: "data.go.kr API 2021-09")
  UNIQUE KEY uniq_church (name, address, region)  -- 동일 지역 내 같은 이름/주소 중복 방지
);

-- Users: 회원 계정 테이블
CREATE TABLE Users (
  user_id    INT PRIMARY KEY AUTO_INCREMENT,
  email      VARCHAR(100) NOT NULL UNIQUE, -- 사용자 이메일 (고유)
  password   VARCHAR(255) NOT NULL,        -- 비밀번호 해시 (SNS 로그인 계정은 NULL 또는 별도 처리)
  name       VARCHAR(50) NOT NULL,         -- 사용자 이름 or 닉네임
  oauth_provider VARCHAR(20) DEFAULT NULL, -- SNS 제공자 (예: kakao, naver, google)
  oauth_id   VARCHAR(100) DEFAULT NULL,    -- SNS 제공자의 고유 ID (소셜 로그인 계정 식별용)
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
  -- 기타: 역할(role: user/admin), 프로필 이미지, 전화번호 등 필요시 컬럼 추가
);

-- Posts: 청빙 게시판 글 테이블
CREATE TABLE Posts (
  post_id      INT PRIMARY KEY AUTO_INCREMENT,
  global_id    CHAR(36) NOT NULL UNIQUE,   -- UUID 등 글로벌 식별자 (A/B 사이트 간 동기화용)
  title        VARCHAR(200) NOT NULL,      -- 글 제목
  content      TEXT NOT NULL,              -- 글 내용 (HTML 또는 Markdown 저장 가능)
  church_id    INT NOT NULL,               -- 연관된 교회 (Foreign Key -> Churches.church_id)
  author_id    INT NOT NULL,               -- 작성자 (Foreign Key -> Users.user_id)
  created_at   DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at   DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  status       VARCHAR(20) NOT NULL DEFAULT 'active',  -- 상태 (active, closed 등)
  FOREIGN KEY (church_id) REFERENCES Churches(church_id),
  FOREIGN KEY (author_id) REFERENCES Users(user_id)
);

-- SiteSync (선택): 사이트 동기화 상태 테이블 (예: 동기화 이력 관리)
CREATE TABLE SiteSyncLog (
  sync_id    INT PRIMARY KEY AUTO_INCREMENT,
  global_id  CHAR(36) NOT NULL,
  source_site VARCHAR(10) NOT NULL,   -- 'A' 또는 'B' 등 출발지 식별
  target_site VARCHAR(10) NOT NULL,   -- 'A' 또는 'B' 등 대상 식별
  action     VARCHAR(10) NOT NULL,    -- 'CREATE' 또는 'UPDATE'
  status     VARCHAR(10) NOT NULL,    -- 'SUCCESS' or 'FAIL'
  timestamp  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  message    VARCHAR(200) NULL        -- 오류 메시지 등 (FAIL 시)
);

위 스키마는 예시이며, 실제 구현 시 각 컬럼의 사이즈나 자료형, 인덱스는 조정할 수 있습니다. 특히 denomination(교단)처럼 공공데이터에 없는 정보는 확장 가능성을 고려해 둔 컬럼입니다. SiteSyncLog는 동기화 작동을 기록하기 위한 부가 테이블로, 동기화 결과를 남겨 추후 문제 발생 시 추적하는 용도입니다.

예시 코드 및 파일 구성

마지막으로, 실제 개발에 바로 활용할 수 있는 예시 코드 조각들을 제시합니다. 각 파일에는 한글/영문 주석을 포함하여 이해를 돕습니다.

1. 교회 정보 API 연동 및 DB 업데이트 스크립트

아래 코드는 공공데이터포털 종교시설 API를 호출하여 교회 정보를 DB에 저장하는 Next.js API Route 예시입니다. (예: pages/api/churches/update.js 파일)

// File: pages/api/churches/update.js
// 설명: 공공데이터 API를 호출하여 교회 정보를 업데이트하는 API (관리자용 또는 내부용)
// Description: API route to fetch church data from public API and update the database.
import { database } from "../../libs/db";  // 가정: DB 연결 객체 (MySQL connection 등)

const API_URL = "http://apis.data.go.kr/5580000/relgnFcltySttusService/getrelgnFcltySttus";
const SERVICE_KEY = process.env.DATA_API_KEY;  // 공공데이터포털 API 인증키 (환경변수에 저장)

export default async function handler(req, res) {
  if (req.method !== "POST") {
    return res.status(405).json({ message: "Method Not Allowed" }); 
  }
  // 권한 체크 (예: 관리자만 실행 가능하도록)
  const user = await getLoggedInUser(req);  // 세션에서 사용자 정보 가져오기 (구현 가정)
  if (!user || !user.isAdmin) {
    return res.status(403).json({ message: "Forbidden" });
  }

  try {
    let totalInserted = 0;
    // 예시: 여러 지역의 API를 순차적으로 호출 (실제로는 지역 리스트를 사전에 정의하거나 데이터포털 메타정보 활용)
    const regions = ["서울특별시 강남구", "서울특별시 관악구", /*... 기타 지역 ...*/];
    for (const region of regions) {
      // 1) 각 지역별 API 호출 (지역에 따라 다른 서비스URL/코드가 있을 수 있어, region 정보를 기반으로 API URL 조정 필요)
      const url = `${API_URL}?serviceKey=${SERVICE_KEY}&RELGN=${encodeURIComponent("기독교")}&perPage=1000&page=1`;
      const response = await fetch(url);
      const data = await response.json();
      if (data.resultCode !== "00") {
        console.error(`API error for region ${region}: ${data.resultMsg}`);
        continue; // 다음 지역으로 계속
      }
      const churches = data.data;  // 가정: 응답 JSON 구조에서 실제 데이터 배열이 data.data 에 있음
      // 2) DB에 교회 데이터 저장/업데이트
      for (const item of churches) {
        const name = item.FCLTY_NM;      // 교회명 (facility name)
        const address = item.RDNMADR;    // 도로명 주소
        const zip = item.ZIP;           // 우편번호
        const phone = item.TELNO || null; // 전화번호 (없으면 null)
        const regionName = region;      // 지역 명 (현재 루프의 지역)
        // DB upsert 처리: 동일 교회명이 같은 주소에 있으면 업데이트, 없으면 새로 삽입
        await database.query(
          `INSERT INTO Churches (name, address, region, zip_code, phone, data_source)
           VALUES (?, ?, ?, ?, ?, ?)
           ON DUPLICATE KEY UPDATE 
             zip_code = VALUES(zip_code), phone = VALUES(phone), data_source = VALUES(data_source)`,
          [name, address, regionName, zip, phone, `API:${region}`]
        );
        totalInserted++;
      }
    }
    return res.status(200).json({ message: `Church data update complete. Total processed: ${totalInserted}` });
  } catch (error) {
    console.error("Church data update failed:", error);
    return res.status(500).json({ message: "Internal Server Error", error: error.message });
  }
}

코드 설명: 위 코드는 관리자 권한으로만 접근 가능한 API 엔드포인트로서, 지역 목록을 가지고 반복하면서 각 지역의 OpenAPI를 호출해 교회 데이터를 받아옵니다. 응답에서 각 교회 데이터를 파싱한 뒤, DB에 ON DUPLICATE KEY UPDATE 구문을 사용하여 있으면 업데이트, 없으면 삽입을 수행합니다. 실제 구현에서는 지역마다 다른 API URL이나 서비스 키가 필요할 수 있어, 사전에 지역별 API endpoint를 구성하거나 공공데이터포털의 메타데이터를 이용해 자동화할 수 있습니다. (예를 들어 서울 관악구 API는 서비스 URL과 서비스 키가 다를 수 있으므로, region별로 식별자가 필요합니다.) 또한 perPage=1000로 가정했지만, 실제 API의 페이지당 최대 건수에 맞춰 조절하고, totalRows를 확인하여 여러 페이지에 걸쳐 루프를 돌며 모든 데이터를 가져와야 할 것입니다.

2. 청빙 게시판 게시글 생성 및 동기화 예시

다음은 게시판 글 작성 API의 예시로, 글 생성 시 상대 사이트로 동기화를 수행하는 로직을 포함합니다. (예: pages/api/posts/index.js 파일의 POST 처리 부분)

// File: pages/api/posts/index.js
// 설명: 게시글 목록 조회 및 새 글 작성 API (POST 요청 시 새로운 청빙 공고 작성 처리)
// Description: Handles fetching post list (GET) and creating a new post (POST). Includes cross-site sync on creation.

import { database } from "../../../libs/db";

export default async function handler(req, res) {
  const user = await getLoggedInUser(req);
  if (!user) {
    return res.status(401).json({ message: "Unauthorized" });  // 로그인 필요
  }

  if (req.method === "GET") {
    // 게시글 목록 조회 (쿼리 파라미터로 페이지네이션 등 처리 가능)
    const results = await database.query("SELECT P.post_id, P.title, C.name as churchName, P.created_at FROM Posts P JOIN Churches C ON P.church_id = C.church_id ORDER BY P.created_at DESC");
    return res.status(200).json(results);
  }

  if (req.method === "POST") {
    const { title, content, churchId } = req.body;
    if (!title || !content || !churchId) {
      return res.status(400).json({ message: "Bad Request: Missing fields" });
    }
    try {
      // 1) DB에 새 게시글 저장
      const globalId = generateUUID();  // 유니크한 global_id 생성 (UUID)
      await database.query(
        `INSERT INTO Posts (global_id, title, content, church_id, author_id) VALUES (?, ?, ?, ?, ?)`,
        [globalId, title, content, churchId, user.user_id]
      );
      const [newPost] = await database.query(`SELECT post_id, title, content, church_id, author_id, global_id, created_at FROM Posts WHERE global_id = ?`, [globalId]);

      // 2) 다른 사이트로 동기화 API 호출 (예: Site B)
      const syncPayload = {
        ...newPost,
        apiKey: process.env.SYNC_API_KEY  // 내부 인증키 포함 (상대 사이트에서 검증용)
      };
      // B사이트의 수신 API URL (환경변수 또는 설정에 저장)
      const targetUrl = process.env.B_SITE_URL + "/api/posts/receive";
      // 비동기로 호출하되, 실패 시 오류 처리
      const response = await fetch(targetUrl, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(syncPayload)
      });
      if (!response.ok) {
        console.error("Failed to sync post to Site B:", await response.text());
        // 동기화 실패 로그 남기기 (DB 또는 파일)
        await database.query(
          `INSERT INTO SiteSyncLog (global_id, source_site, target_site, action, status, message) 
           VALUES (?, 'A', 'B', 'CREATE', 'FAIL', ?)`,
          [globalId, `HTTP ${response.status}`]
        );
      } else {
        // 동기화 성공 로그
        await database.query(
          `INSERT INTO SiteSyncLog (global_id, source_site, target_site, action, status) 
           VALUES (?, 'A', 'B', 'CREATE', 'SUCCESS')`,
          [globalId]
        );
      }

      return res.status(201).json({ message: "Post created", post: newPost });
    } catch (err) {
      console.error("Error creating post:", err);
      return res.status(500).json({ message: "Internal Server Error" });
    }
  }

  // 기타 메서드 허용 안함
  return res.status(405).json({ message: "Method Not Allowed" });
}

코드 설명: 위 API는 GET 요청 시 게시글 리스트를, POST 요청 시 새 글을 생성합니다. POST 처리과정에서 **global_id(UUID)**를 생성하여 게시글과 함께 저장하고, 곧바로 상대 사이트(B)의 /api/posts/receive 엔드포인트로 fetch를 통해 POST 요청을 보냅니다. 이 요청에는 새 글의 모든 데이터와, 사전에 공유한 SYNC_API_KEY를 포함시켜 인증에 활용합니다. 동기화 요청에 대한 응답이 실패할 경우 오류 내용을 SiteSyncLog에 남겨놓고 로그로도 출력합니다. 응답이 성공이면 동기화 성공으로 로그를 남깁니다. (참고로, 동기화 API 호출은 여기에서는 동기적으로 처리했지만, 실제 서비스에서는 응답 속도를 위해 비동기 작업이나 메시지 큐로 처리하고, 사용자에게는 글 생성 성공만 빠르게 응답하는 방안도 고려할 수 있습니다.)

3. 동기화 수신 API 예시 (상대 사이트 B)

한편, B사이트에는 A사이트로부터의 요청을 받아 게시글을 생성/수정하는 수신용 API가 필요합니다. 아래는 B사이트의 pages/api/posts/receive.js에 해당하는 예시 코드입니다.

// File: pages/api/posts/receive.js  (Site B의 동기화 수신 API)
// 설명: 다른 사이트(A)로부터 전달된 게시글 생성/수정 요청을 받아 처리
// Description: Receives post data from Site A and creates/updates the post in Site B's database.

import { database } from "../../../libs/db";

export default async function handler(req, res) {
  if (req.method !== "POST") {
    return res.status(405).json({ message: "Method Not Allowed" });
  }
  const { global_id, title, content, church_id, author_id, created_at, apiKey } = req.body;
  // 1) 인증키 검증
  if (!apiKey || apiKey !== process.env.SYNC_API_KEY) {
    return res.status(401).json({ message: "Unauthorized - invalid API key" });
  }
  if (!global_id || !title || !content) {
    return res.status(400).json({ message: "Bad Request - missing fields" });
  }
  try {
    // 2) global_id로 기존 글 존재 여부 확인
    const [existing] = await database.query(`SELECT post_id FROM Posts WHERE global_id = ?`, [global_id]);
    if (existing) {
      // 이미 존재하면 업데이트 (수정 동기화의 경우)
      await database.query(
        `UPDATE Posts SET title=?, content=?, church_id=?, author_id=? WHERE global_id=?`,
        [title, content, church_id, author_id, global_id]
      );
    } else {
      // 존재하지 않으면 신규 생성 (생성 동기화의 경우)
      await database.query(
        `INSERT INTO Posts (global_id, title, content, church_id, author_id, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
        [global_id, title, content, church_id, author_id, created_at]
      );
    }
    return res.status(200).json({ message: "Sync post processed" });
  } catch (err) {
    console.error("Error processing synced post:", err);
    return res.status(500).json({ message: "Internal Server Error" });
  }
}

코드 설명: 이 수신 API는 A사이트에서 전송한 global_id, title, content, church_id 등의 데이터를 받아 DB에 저장하거나 업데이트합니다. 수신 시 먼저 API 키로 요청을 검증하며, global_id를 키로 DB를 조회하여 이미 존재하면 업데이트(즉, A사이트에서 수정된 경우 B에서도 수정), 없으면 새로 삽입합니다. 삽입 시에는 가능한 A사이트의 created_at을 그대로 사용하여 양쪽 사이트에 시간 정보가 일치하도록 처리합니다. (DB에서 created_at에 DEFAULT CURRENT_TIMESTAMP가 설정돼있지만, 동기화 시에는 명시적으로 값을 넣어 동일하게 유지)

이러한 구조로 구현하면, A와 B 어디서 글을 작성하거나 수정하든 양측 DB에 내용이 반영되어 사용자들은 동일한 게시판 내용을 보게 됩니다. 추후 PHP로 전환할 때도 이와 동일한 REST API 방식과 DB스키마를 사용하면 크게 변경 없이 연동을 지속할 수 있습니다.

 

 

*구축예정인 구축로드맵 프로젝트