Search

Matched domain: ryuda.github.io

IP = 185.199.109.153

robots.txt

// tools/scan-apis.mjs
// Spring(Java 중심) 엔드포인트 스캐너
// - @RestController / @Controller + @RequestMapping / @*Mapping 탐지
// - description: @Operation.description ⇒ 메서드 위 /** ... */의 "description:" ⇒ 클래스 @Tag.description
// - version: 경로 선두 v1/v2/... 추출
// - RequestBody 관련 정보는 출력하지 않음
// - 결과: 프로젝트 루트에 .api-scan.json 저장

import fs from 'fs';
import path from 'path';

const root = process.cwd();
const SRC_DIRS = ['src', 'src/main/java']; // 필요하면 'src/main/kotlin' 추가
const TARGET_EXTS = new Set(['.java']);     // Java 위주. Kotlin 쓰면 '.kt'도 추가

// ---------- 공통 유틸 ----------
const readText = (f) => { try { return fs.readFileSync(f, 'utf8'); } catch { return ''; } };
const normPath = (p) => (p || '').replace(/\/+/g, '/').replace(/\/$/, '') || '/';
const indexToLine = (text, idx) => text.slice(0, idx).split('\n').length;
const extractVersion = (p) => {
  const m = /^\/?(v\d+)\b/i.exec(p || '');
  return m ? m[1].toLowerCase() : null;
};

const walk = (dir, out = []) => {
  if (!fs.existsSync(dir)) return out;
  for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
    if (ent.name.startsWith('.')) continue;
    const p = path.join(dir, ent.name);
    if (ent.isDirectory()) walk(p, out);
    else {
      const ext = path.extname(ent.name);
      if (TARGET_EXTS.has(ext)) out.push(p);
    }
  }
  return out;
};

// ---------- description 추출 ----------
// 1) 메서드 주변 @Operation(description="...")
const findOperationDescription = (text, idx) => {
  const around = text.slice(Math.max(0, idx - 2000), idx + 2000);
  const m = around.match(/@Operation\s*\(\s*[^)]*description\s*=\s*"(.*?)"/s);
  return m ? (m[1] || '').trim() : '';
};

// 2) 매핑 직전 /** ... */ 블록에서 swagger 스타일 "description:" 찾아보기
const findNearestBlockDescription = (text, idx) => {
  // idx 이전의 가장 가까운 /** ... */ 블록
  const before = text.slice(0, idx);
  const start = before.lastIndexOf('/**');
  if (start === -1) return '';
  const end = before.indexOf('*/', start + 3);
  if (end === -1) return '';
  const block = before.slice(start, end + 2);

  // 블록 내부에서 yaml 스타일 description: "..."
  // (큰따옴표/작은따옴표 모두, 멀티라인은 간단 처리)
  const m1 = block.match(/description\s*:\s*"(.*?)"/s);
  if (m1) return (m1[1] || '').trim();
  const m2 = block.match(/description\s*:\s*'(.*?)'/s);
  if (m2) return (m2[1] || '').trim();

  // 혹시 줄 끝 주석형: description: 텍스트
  const m3 = block.match(/description\s*:\s*(.+)\n/);
  if (m3) return (m3[1] || '').trim();

  return '';
};

// 3) 클래스 수준 @Tag(description="...")
const findClassTagDescription = (classText) => {
  const m = classText.match(/@Tag\s*\(\s*[^)]*description\s*=\s*"(.*?)"/s);
  return m ? (m[1] || '').trim() : '';
};

// ---------- content-type(consumes) 추출 ----------
const findConsumesNear = (text, idx) => {
  const around = text.slice(Math.max(0, idx - 400), idx + 400);
  // 메서드 매핑의 consumes="..."
  const m1 = around.match(/consumes\s*=\s*"(.*?)"/);
  if (m1) return (m1[1] || '').trim();
  return null;
};
const findConsumesInClass = (classText) => {
  const m = classText.match(/@RequestMapping\s*\(\s*[^)]*consumes\s*=\s*"(.*?)"/);
  return m ? (m[1] || '').trim() : null;
};

// ---------- 클래스 텍스트 추출 ----------
const findEnclosingClassText = (text, methodIdx) => {
  // 매우 단순한 휴리스틱: methodIdx 이전에서 가장 가까운 "class XXX {" 찾고,
  // 그 뒤로 닫는 } 까지를 클래스 블록으로 간주
  const before = text.slice(0, methodIdx);
  const classStart = before.lastIndexOf('class ');
  if (classStart === -1) return '';
  // 여길 기준으로 이후 전체를 보고 첫 번째 '{'와 매칭되는 '}' 찾기
  const fromClass = text.slice(classStart);
  const braceOpen = fromClass.indexOf('{');
  if (braceOpen === -1) return '';
  let depth = 0;
  for (let i = braceOpen; i < fromClass.length; i++) {
    const ch = fromClass[i];
    if (ch === '{') depth++;
    else if (ch === '}') {
      depth--;
      if (depth === 0) {
        return fromClass.slice(0, i + 1);
      }
    }
  }
  return '';
};

// ---------- 메인 스캔 ----------
const files = SRC_DIRS.flatMap((d) => walk(path.join(root, d)));
const endpoints = [];

for (const file of files) {
  const text = readText(file);
  if (!text) continue;
  const rel = path.relative(root, file);

  // 컨트롤러 클래스만 진행
  if (!/@(RestController|Controller)\b/.test(text)) continue;

  // 클래스 레벨 prefix (@RequestMapping)
  let classPrefix = '';
  const cp =
    [...text.matchAll(/@RequestMapping\s*\(\s*(?:value\s*=\s*)?['"`]([^'"`]+)['"`]\s*\)/g)].pop() ||
    [...text.matchAll(/@RequestMapping\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g)].pop();
  if (cp) classPrefix = (cp[1] || '').trim();

  // 메서드 매핑
  const mapRe = /@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\s*\(\s*(['"`]?)([^'"`)]*)\2\s*\)[\s\S]*?\{/g;
  for (const m of text.matchAll(mapRe)) {
    const idx = m.index || 0;
    const method = m[1].replace('Mapping', '').toUpperCase();
    const sub = (m[3] || '').trim();
    const full = normPath('/' + [classPrefix, sub].filter(Boolean).join('/'));
    const version = extractVersion(full);

    // description: @Operation ⇒ 블록 "description:" ⇒ @Tag
    let description = findOperationDescription(text, idx);
    if (!description) {
      description = findNearestBlockDescription(text, idx);
      if (!description) {
        const classText = findEnclosingClassText(text, idx);
        description = findClassTagDescription(classText);
      }
    }

    // contentType: 메서드 consumes ⇒ 클래스 consumes ⇒ 기본 application/json
    let contentType = findConsumesNear(text, idx);
    if (!contentType) {
      const classText = findEnclosingClassText(text, idx);
      contentType = findConsumesInClass(classText) || 'application/json';
    }

    endpoints.push({
      framework: 'spring',
      method,
      path: full,
      version,
      file: rel,
      line: indexToLine(text, idx),
      description,
      contentType
    });
  }
}

// 중복 제거
const seen = new Set();
const out = [];
for (const e of endpoints) {
  const k = `${e.framework}|${e.method}|${e.path}|${e.file}|${e.line}`;
  if (seen.has(k)) continue;
  seen.add(k);
  out.push(e);
}

// 저장
const outPath = path.join(root, '.api-scan.json');
fs.writeFileSync(outPath, JSON.stringify(out, null, 2));
console.log(`Found ${out.length} endpoints -> ${outPath}`);

Look up this url in the url tool https://ryuda.github.io/.well-known/acme-challenge: 404 text/html; charset=utf-8
https://ryuda.github.io/.well-known/csvm: 404 text/html; charset=utf-8
https://ryuda.github.io/.well-known/nostr.json: 404 text/html; charset=utf-8
https://ryuda.github.io/.well-known/security.txt: 404 text/html; charset=utf-8
https://ryuda.github.io/.well-known/traffic-advice: 404 text/html; charset=utf-8