Webhook 연동 가이드

개요

파인핏 Biz에서 분석 대상자에게 email 등으로 전송할 때, 분석 결과 데이터가 webhook을 통해 고객사 서버로 전송됩니다.

API 사용 신청 및 승인 과정

1. API 사용 신청

CenterAdmin이 Remo-API-SERVER에 API 사용을 신청합니다.

2. 사용 승인

Remo에서 API 사용 신청을 검토하고 승인합니다.

3. API Credential 발행

사용 승인된 CenterAdmin이 본인이 사용할 API credential을 발행합니다.

4. Webhook URL 설정

발행한 API credential에 webHookUrl을 설정합니다.

5. Webhook 데이터 전송

파인핏 Biz에서 분석 대상자에게 email 등으로 전송하기를 실행했을 때, 해당 데이터가 크레덴셜에 설정한 webHookUrl로 전송됩니다.

프로세스 흐름도

CenterAdmin
Remo-API-SERVER
Remo
1단계: API 사용 신청 및 승인
승인 완료
CenterAdmin
API Credential 발행
2단계: Credential 발행
CenterAdmin
webHookUrl 설정
3단계: Webhook URL 설정
파인핏 Biz
고객사 서버
4단계: Webhook 데이터 전송

흐름도 설명

Webhook 수신 예시 코드

Node.js Express 예시

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

// JSON 파싱을 위한 미들웨어
app.use(express.json({ limit: '10mb' }));

// Webhook 엔드포인트
app.post('/webhook', async (req, res) => {
    try {
        console.log('Webhook 수신:', new Date().toISOString());
        
        // 요청 본문에서 데이터 추출
        const webhookData = req.body;
        
        // WebhookDataResponseDto 구조
        const {
            shapeMeasure,        // 체형 측정 데이터 (선택적)
            poseMeasure,         // 자세 측정 데이터 (선택적)
            screeningParticipant, // 검사 참가자 정보 (필수)
            screeningCenter,     // 검사 센터 정보 (필수)
            webhookUrl,          // 웹훅 URL (필수)
            tag                  // 웹훅 태그 (필수)
        } = webhookData;
        
        // tag를 통한 이벤트 타입 확인
        console.log('Webhook 태그:', tag);
        
        // 참가자 정보 처리
        if (screeningParticipant) {
            console.log('참가자 정보:', {
                이름: screeningParticipant.name,
                전화번호: screeningParticipant.phone,
                이메일: screeningParticipant.email
            });
        }
        
        // 센터 정보 처리
        if (screeningCenter) {
            console.log('센터 정보:', {
                관리자이메일: screeningCenter.email
            });
        }
        
        // 체형 측정 데이터 처리
        if (shapeMeasure) {
            console.log('체형 측정 데이터 수신:', {
                키: `${shapeMeasure.height_mm}µm`,
                몸무게: `${shapeMeasure.weight_g}µg`,
                체지방률: `${shapeMeasure.body_fat_percentage}%`,
                체형분류: shapeMeasure.shape_class,
                체형크기: shapeMeasure.shape_size
            });
            
            // 데이터베이스에 저장
            await saveShapeMeasureData(shapeMeasure);
        }
        
        // 자세 측정 데이터 처리
        if (poseMeasure) {
            console.log('자세 측정 데이터 수신:', {
                정면기울기: `${poseMeasure.far_tilt_m_}도`,
                측면기울기: `${poseMeasure.sar_tilt_m_}도`,
                거북목여부: poseMeasure.sar_fwd_head ? '예' : '아니오',
                라운드숄더: `${poseMeasure.round_shoulder_m_}도`
            });
            
            // 좌표 데이터 파싱 (JSONString으로 전송되므로 JSON.parse() 필요)
            if (poseMeasure.far_coords) {
                const farCoords = JSON.parse(poseMeasure.far_coords);
                console.log('정면 2D 좌표 데이터:', farCoords);
            }
            if (poseMeasure.far_coords3D) {
                const farCoords3D = JSON.parse(poseMeasure.far_coords3D);
                console.log('정면 3D 좌표 데이터:', farCoords3D);
            }
            if (poseMeasure.sar_coords) {
                const sarCoords = JSON.parse(poseMeasure.sar_coords);
                console.log('측면 2D 좌표 데이터:', sarCoords);
            }
            if (poseMeasure.sar_coords3D) {
                const sarCoords3D = JSON.parse(poseMeasure.sar_coords3D);
                console.log('측면 3D 좌표 데이터:', sarCoords3D);
            }
            
            // 데이터베이스에 저장
            await savePoseMeasureData(poseMeasure);
        }
        
        // 성공 응답 (200 OK)
        res.status(200).json({
            success: true,
            message: 'Webhook 데이터 수신 완료',
            timestamp: new Date().toISOString(),
            receivedData: {
                hasShapeMeasure: !!shapeMeasure,
                hasPoseMeasure: !!poseMeasure,
                participantCount: 1,
                tag: tag
            }
        });
        
    } catch (error) {
        console.error('Webhook 처리 오류:', error);
        
        // 오류 응답 (500 Internal Server Error)
        res.status(500).json({
            success: false,
            message: 'Webhook 데이터 처리 중 오류 발생',
            error: error.message,
            timestamp: new Date().toISOString()
        });
    }
});

// 데이터베이스 저장 함수 예시
async function saveShapeMeasureData(shapeMeasure) {
    // MongoDB, PostgreSQL 등에 데이터 저장
    console.log('체형 측정 데이터 저장 중...');
    // await db.collection('shapeMeasures').insertOne(shapeMeasure);
}

async function savePoseMeasureData(poseMeasure) {
    // MongoDB, PostgreSQL 등에 데이터 저장
    console.log('자세 측정 데이터 저장 중...');
    // await db.collection('poseMeasures').insertOne(poseMeasure);
}

// 서버 시작
app.listen(port, () => {
    console.log(`Webhook 서버가 포트 ${port}에서 실행 중입니다.`);
    console.log(`Webhook 엔드포인트: http://localhost:${port}/webhook`);
});

// 헬스체크 엔드포인트
app.get('/health', (req, res) => {
    res.status(200).json({
        status: 'OK',
        timestamp: new Date().toISOString(),
        uptime: process.uptime()
    });
});

환경 설정

# package.json 생성
npm init -y

# 필요한 패키지 설치
npm install express

# 개발 의존성 설치
npm install --save-dev nodemon

# 스크립트 추가 (package.json)
{
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js"
  }
}

실행 방법

# 개발 모드로 실행 (자동 재시작)
npm run dev

# 프로덕션 모드로 실행
npm start

주의사항

  1. HTTPS 필수: Remo는 HTTPS 프로토콜만 지원합니다
  2. 응답 시간: 3초 이내에 응답해야 합니다
  3. 데이터 검증: 수신된 데이터의 유효성을 검증하세요
  4. 에러 처리: 적절한 에러 처리와 로깅을 구현하세요
  5. 보안: 인증 및 권한 검사를 고려하세요

연동 신청

분석결과 webhook을 받기 위해서는 Remo로 연락 바랍니다.

필요 사항

webhook 연동을 위해 다음 정보가 필요합니다:

  1. 고객사 서버: webhook을 받을 서버
  2. POST Method URL: 해당 서버에서 webhook을 받을 엔드포인트 URL

제한 사항

데이터 전송 시점

파인핏 Biz에서 분석 대상자에게 email 등으로 전송하기를 실행했을 때 해당 데이터가 전송됩니다.

전송 데이터 구조

전송되는 데이터는 WebhookDataResponseDto 구조를 따릅니다:

{
  shapeMeasure: ShapeMeasureResponseDto,        // 체형 측정 데이터 (선택적)
  poseMeasure: PoseMeasureResponseDto,          // 자세 측정 데이터 (선택적)
  screeningParticipant: ScreeningParticipantWebhookDto,  // 검사 참가자 정보 (필수)
  screeningCenter: ScreeningCenterWebhookDto,            // 검사 센터 정보 (필수)
  webhookUrl: string,                            // 웹훅 URL (필수)
  tag: string                                    // 웹훅 태그 (필수) - RESOURCE:ACTION 형식
}

데이터 상세 설명

WebhookDataResponseDto

tag 속성

tag 속성은 웹훅이 전송되는 곳의 리소스와 행위를 나타내는 식별자입니다.

형식: RESOURCE:ACTION

예시:

용도:

ScreeningParticipantWebhookDto (검사 참가자 정보)

검사에 참가한 사용자의 기본 정보를 포함합니다.

객체 구조:

{
  phone: string,                  // 참가자 전화번호 (필수)
  name: string,                   // 참가자 이름 (필수)
  email?: string                  // 참가자 이메일 (선택적, null 가능)
}

필드 설명:

ScreeningCenterWebhookDto (검사 센터 정보)

검사를 수행한 센터의 관리자 정보를 포함합니다.

객체 구조:

{
  email: string                   // 센터 관리자 이메일 (필수)
}

필드 설명:

ShapeMeasureResponseDto (체형 측정 데이터)

객체 구조:

{
  id: number,                    // 고유 ID
  uuid: string,                  // UUID
  createdAt: Date,               // 생성일
  updatedAt: Date,               // 수정일
  deletedAt: Date,               // 삭제일 (선택적)

  // 기본 신체 정보
  height_mm: number,             // 키 (µm 단위)
  weight_g: number,              // 몸무게 (µg 단위)
  skeletal_muscle_mass_g: number, // 골격근량 (g 단위)
  body_fat_mass_g: number,       // 체지방량 (g 단위)
  body_fat_percentage: number,   // 체지방률 (%)
  abdominal_fat_percentage: number, // 복부지방률 (%)

  // 이미지 URL
  command: string,               // AI 분석 요청 명령어
  state: number,                 // AI 분석 처리 결과 상태
  front_original_image: string,  // 정면 사진 원본 URL
  side_original_image: string,   // 측면 사진 원본 URL
  frontCaptureImage: string,     // 프론트 렌더링 결과 이미지 URL

  // 백분율 정보
  height_per: number,            // 키 백분율 (0-100)
  weight_per: number,            // 몸무게 백분율 (0-100)

  // 둘레 측정값 (mm 단위)
  chest: number,                 // 가슴 둘레
  chest_max: number,             // 가슴 최대 둘레
  chest_min: number,             // 가슴 최소 둘레
  chest_per: number,             // 가슴 둘레 백분율
  waist: number,                 // 허리 둘레
  waist_max: number,             // 허리 최대 둘레
  waist_min: number,             // 허리 최소 둘레
  waist_per: number,             // 허리 둘레 백분율
  hip: number,                   // 엉덩이 둘레
  hip_max: number,               // 엉덩이 최대 둘레
  hip_min: number,               // 엉덩이 최소 둘레
  hip_per: number,               // 엉덩이 둘레 백분율
  thigh: number,                 // 허벅지 둘레
  thigh_max: number,             // 허벅지 최대 둘레
  thigh_min: number,             // 허벅지 최소 둘레
  thigh_per: number,             // 허벅지 둘레 백분율

  // 체형 분류
  shape_class: number,           // 체형 클래스 (0: 사각형, 1: 둥근형, 2: 삼각형, 3: 역삼각형, 4: 모래시계형)
  shape_size: number,            // 체형 크기 (0: 매우 마름, 1: 마름, 2: 표준, 3: 과체중, 4: 비만)

  // 팔 길이 측정값 (mm 단위)
  arm_length: number,            // 양팔 평균 길이
  arm_per: number,               // 양팔 길이 백분율
  left_up_arm_length: number,    // 왼쪽 위팔 길이
  right_up_arm_length: number,   // 오른쪽 위팔 길이
  left_down_arm_length: number,  // 왼쪽 아래팔 길이
  right_down_arm_length: number, // 오른쪽 아래팔 길이
  left_arm_length: number,       // 왼쪽 팔 길이
  right_arm_length: number,      // 오른쪽 팔 길이

  // 다리 길이 측정값 (mm 단위)
  leg_length: number,            // 양다리 평균 길이
  leg_per: number,               // 양다리 길이 백분율
  left_up_leg_length: number,    // 왼쪽 위다리 길이
  right_up_leg_length: number,   // 오른쪽 위다리 길이
  left_down_leg_length: number,  // 왼쪽 아래다리 길이
  right_down_leg_length: number, // 오른쪽 아래다리 길이
  left_leg_length: number,       // 왼쪽 다리 길이
  right_leg_length: number,      // 오른쪽 다리 길이

  // 기타 길이 측정값 (mm 단위)
  lower_body_length: number,     // 하체 길이
  trunk_length: number,          // 상체 길이
  head_length: number,           // 머리 길이
  body_length: number            // 전신 길이
}

PoseMeasureResponseDto (자세 측정 데이터)

객체 구조:

{
  id: number,                    // 고유 ID
  uuid: string,                  // UUID
  createdAt: Date,               // 생성일
  updatedAt: Date,               // 수정일
  deletedAt: Date,               // 삭제일 (선택적)

  // 기본 신체 정보
  height_mm: number,             // 키 (µm 단위)
  weight_g: number,              // 몸무게 (µg 단위)
  skeletal_muscle_mass_g: number, // 골격근량 (g 단위)
  body_fat_mass_g: number,       // 체지방량 (g 단위)
  body_fat_percentage: number,   // 체지방률 (%)
  abdominal_fat_percentage: number, // 복부지방률 (%)

  // 이미지 URL
  command: string,               // AI 분석 요청 명령어
  state: number,                 // AI 분석 처리 결과 상태
  front_original_image: string,  // 정면 사진 원본 URL
  side_original_image: string,   // 측면 사진 원본 URL
  frontCaptureImage: string,     // 프론트 렌더링 결과 이미지 URL

  // 정면 분석 데이터 (far: front)
  far_coords: Array<Array<number>>,      // 정면 골격 2D 좌표 배열 (JSONString으로 전송되므로 JSON.parse() 필요)
  far_coords3D: Array<Array<number>>,    // 정면 골격 3D 좌표 배열 (JSONString으로 전송되므로 JSON.parse() 필요)
  far_tilt_m_: number,                   // 정면 사진 좌우 기울기 각도 (밀리도 단위)
  far_head_bal_m_: number,               // 정면 머리 좌우 기울기 각도 (밀리도 단위)
  far_shoulder_bal_m_: number,           // 정면 어깨 기울기 밸런스 각도 (밀리도 단위)
  far_pelvic_bal_m_: number,             // 정면 골반 기울기 밸런스 각도 (밀리도 단위)
  far_knee_bal_m_: number,               // 정면 무릎 기울기 밸런스 각도 (밀리도 단위)
  far_right_QAngle_m_: number,           // 정면 오른쪽 다리 Q값 (O다리 각도, 밀리도 단위)
  far_left_QAngle_m_: number,            // 정면 왼쪽 다리 Q값 (O다리 각도, 밀리도 단위)

  // 측면 분석 데이터 (sar: side)
  sar_coords: Array<Array<number>>,      // 측면 골격 2D 좌표 배열 (JSONString으로 전송되므로 JSON.parse() 필요)
  sar_coords3D: Array<Array<number>>,    // 측면 골격 3D 좌표 배열 (JSONString으로 전송되므로 JSON.parse() 필요)
  sar_tilt_m_: number,                   // 측면 사진 앞뒤 기울기 각도 (밀리도 단위)
  sar_head_tilt_m_: number,              // 측면 머리 앞뒤 기울기 각도 (밀리도 단위)
  sar_fwd_head: number,                  // 거북목 여부 (0: 거짓, 1: 참)
  turtle_neck_m_: number,                // 거북목 기울기 각도 (밀리도 단위)
  round_shoulder_m_: number,             // 라운드 숄더 기울기 각도 (밀리도 단위)
  sar_pelvic_tilt_m_: number            // 골반 전후경사 기울기 각도 (밀리도 단위, -: 앞으로, +: 뒤로)
}

연동 문의

webhook 연동에 대한 자세한 내용이나 기술 지원이 필요한 경우, 아래 연락처로 문의해 주세요:

Remo 기술지원팀

주의사항

  1. 보안: HTTPS 프로토콜 사용을 권장합니다
  2. 응답 시간: webhook 전송 후 3초 이내에 응답을 받아야 합니다
  3. 에러 처리: webhook 전송 실패 시에도 파인핏 Biz 서비스는 정상적으로 동작합니다
  4. 데이터 보관: 전송된 데이터는 고객사에서 적절히 보관 및 관리해야 합니다