The breeze is breaking
The breeze is breaking
파트너사와 공존 및 협력하는 새로운 시스템 도입 시대, 디지털 노동력으로 기업의 역량을 무한히 확장시키는 방법을 알아보세요. VIW로 여러분의 상상을 지금 현실로 만들어 보세요.
The breeze is breaking
항목 | 설명 |
---|---|
Node.js | 최소 v16 이상 (권장: v18) 설치 필요 |
npm 또는 yarn | 패키지 매니저 사용 가능하도록 설치 |
VS Code | 코드 편집기 (선택) |
FFmpeg | Remotion이 내부적으로 사용하므로 설치 필수 |
먼저 Node.js 공식 사이트에서 LTS 버전(예: 18.x)을 다운로드 후 설치하세요.
설치 완료 후 명령 프롬프트(cmd 또는 PowerShell)에서 확인:
node -v
npm -v
Remotion은 내부적으로 FFmpeg를 사용하므로 반드시 설치해야 합니다.
ffmpeg-release-full.7z
파일 다운로드bin
폴더의 경로를 시스템 환경 변수 PATH에 추가예시 경로: C:\ffmpeg\bin
ffmpeg -version
출력이 나온다면 정상 설치된 것입니다.
PowerShell 또는 cmd에서 아래 명령어로 Remotion 프로젝트를 생성합니다.
npx create-video my-video-project
위 명령어는 create-video
CLI를 통해 Remotion 템플릿 프로젝트를 생성합니다.
생성된 디렉토리로 이동:
cd my-video-project
my-video-project/
├── public/ # 정적 파일 (이미지, 음악 등)
├── src/
│ ├── index.tsx # 컴포넌트 정의
│ └── Root.tsx
├── package.json
└── remotion.config.ts
public/
폴더에 이미지 넣기예시 이미지: image1.jpg
, image2.jpg
, image3.jpg
등을 public/
폴더에 복사합니다.
src/index.tsx
수정import { AbsoluteFill, Img, Sequence, useVideoConfig } from "remotion";
import React from "react";
const images = [
"/image1.jpg",
"/image2.jpg",
"/image3.jpg"
];
export const MyVideo = () => {
const { width, height } = useVideoConfig();
return (
<AbsoluteFill>
{images.map((src, i) => (
<Sequence key={src} from={i * 30} durationInFrames={30}>
<AbsoluteFill>
<Img src={src} style={{ width, height, objectFit: "cover" }} />
</AbsoluteFill>
</Sequence>
))}
</AbsoluteFill>
);
};
objectFit="cover"
로 비율 유지하면서 꽉 채워서 보여줍니다.remotion.config.ts
에서 기본 설정을 조정할 수 있습니다:
import { Config } from "remotion";
Config.Rendering.setImageFormat("jpeg");
Config.Output.setLocation("out/video.mp4");
Config.Puppeteer.setLaunchPuppeteerSettings({
headless: true,
});
npm run dev
브라우저에서 http://localhost:3000
접속하면 영상 미리보기가 됩니다.
npm run build
또는 특정 출력 옵션 지정:
npx remotion render MyVideo out/video.mp4 --props='{}'
렌더링 완료되면 out/
폴더에 video.mp4
가 생성됩니다.
out/video.mp4
를 영상 플레이어에서 열어보세요!
crossfade
)<Audio src="/music.mp3" />
)필요하시면 아래 내용도 알려주세요:
그에 따라 더 구체적인 예제 코드도 제공해 드릴 수 있어요 😊
준비가 되셨다면, 지금 바로 Remotion으로 첫 번째 영상을 만들어 보세요!
궁금한 점 있으시면 언제든지 물어보세요 💪
SELECT CONCAT('Hello', ' ', 'World') AS combined_string;
-- 결과: 'Hello World'
SELECT LPAD('123', 5, '0') AS padded_number;
-- 결과: '00123'
테스트 계층 구조를 위한 AG-Grid 구현 예제
react 17.2.4, typescript 4.3.5, ag-grid 26.1.0, openjdk 17, spring, ibatis, mariadb 기술 스택을 기반으로 테스트 계층 구조(테스트계획-테스트시나리오-테스트케이스-테스트액티비티)를 하나의 AG-Grid에 표시하고 표시/숨김 기능 있고 그리드에서도 +,- 항목이 있어서 표시/숨김 기능이 있도록 소스 생성해주세요
"테스트계층 구조(테스트계획 → 시나리오 → 케이스 → 액티비티)"를 하나의 AG-Grid에 표시 + 표시/숨김 토글 + +, -로 트리 확장/축소 가능한 예제
프로젝트의 요구사항을 고려했을 때, 효율적인 테스트 도구 시스템을 구축하기 위한 데이터베이스 설계 및 주요 로직 단계를 Java, Spring, iBatis, MariaDB 환경에 맞춰 상세히 정리해 드릴게요.
테스트 도구의 다양한 파라미터와 계층 구조를 효율적으로 관리하기 위해 다음 테이블들을 설계할 수 있습니다.
TEST_PLAN
테이블컬럼:
PLAN_ID
(BIGINT PK, AUTO\_INCREMENT): 테스트 계획 IDPLAN_NAME
(VARCHAR(255) NOT NULL): 테스트 계획 이름DESCRIPTION
(TEXT): 테스트 계획 설명CREATED_AT
(TIMESTAMP DEFAULT CURRENT\_TIMESTAMP): 생성일시UPDATED_AT
(TIMESTAMP DEFAULT CURRENT\_TIMESTAMP ON UPDATE CURRENT\_TIMESTAMP): 수정일시TEST_SCENARIO
테이블컬럼:
SCENARIO_ID
(BIGINT PK, AUTO\_INCREMENT): 테스트 시나리오 IDPLAN_ID
(BIGINT FK): 소속 테스트 계획 ID (TEST_PLAN.PLAN_ID
참조)SCENARIO_NAME
(VARCHAR(255) NOT NULL): 테스트 시나리오 이름DESCRIPTION
(TEXT): 테스트 시나리오 설명CREATED_AT
(TIMESTAMP DEFAULT CURRENT\_TIMESTAMP): 생성일시UPDATED_AT
(TIMESTAMP DEFAULT CURRENT\_TIMESTAMP ON UPDATE CURRENT\_TIMESTAMP): 수정일시TEST_CASE
테이블컬럼:
CASE_ID
(BIGINT PK, AUTO\_INCREMENT): 테스트 케이스 IDSCENARIO_ID
(BIGINT FK): 소속 테스트 시나리오 ID (TEST_SCENARIO.SCENARIO_ID
참조)CASE_NAME
(VARCHAR(255) NOT NULL): 테스트 케이스 이름DESCRIPTION
(TEXT): 테스트 케이스 설명CREATED_AT
(TIMESTAMP DEFAULT CURRENT\_TIMESTAMP): 생성일시UPDATED_AT
(TIMESTAMP DEFAULT CURRENT\_TIMESTAMP ON UPDATE CURRENT\_TIMESTAMP): 수정일시TEST_ACTIVITY
테이블컬럼:
ACTIVITY_ID
(BIGINT PK, AUTO\_INCREMENT): 테스트 활동 IDCASE_ID
(BIGINT FK): 소속 테스트 케이스 ID (TEST_CASE.CASE_ID
참조)ACTIVITY_NAME
(VARCHAR(255) NOT NULL): 테스트 활동 이름SQL_TEMPLATE
(LONGTEXT NOT NULL): 실행될 SQL 템플릿 (파라미터 포함)CREATED_AT
(TIMESTAMP DEFAULT CURRENT\_TIMESTAMP): 생성일시UPDATED_AT
(TIMESTAMP DEFAULT CURRENT\_TIMESTAMP ON UPDATE CURRENT\_TIMESTAMP): 수정일시PARAMETER_DEFINITION
테이블컬럼:
PARAM_DEF_ID
(BIGINT PK, AUTO\_INCREMENT): 파라미터 정의 IDREFER_ID
(BIGINT NOT NULL): 참조하는 엔티티 ID (Plan ID, Scenario ID, Case ID, Activity ID)REFER_TYPE
(VARCHAR(20) NOT NULL): 참조하는 엔티티 타입 (PLAN
, SCENARIO
, CASE
, ACTIVITY
)PARAM_NAME
(VARCHAR(100) NOT NULL): 파라미터 이름 (예: testId
, ADD_CONDITION_1
, userId
)PARAM_TYPE
(VARCHAR(50) NOT NULL): 파라미터 타입 (SYSTEM
, CONDITIONAL
, STATIC
, DYNAMIC
, SQL_QUERY
)PARAM_VALUE
(TEXT): 파라미터 값 (고정값, SQL 쿼리, 동적 SQL 등. 타입에 따라 의미 다름)DESCRIPTION
(TEXT): 파라미터 설명CREATED_AT
(TIMESTAMP DEFAULT CURRENT\_TIMESTAMP): 생성일시UPDATED_AT
(TIMESTAMP DEFAULT CURRENT\_TIMESTAMP ON UPDATE CURRENT\_TIMESTAMP): 수정일시TEST_EXECUTION_RESULT
테이블컬럼:
EXEC_ID
(BIGINT PK, AUTO\_INCREMENT): 실행 결과 IDACTIVITY_ID
(BIGINT FK): 실행된 테스트 활동 ID (TEST_ACTIVITY.ACTIVITY_ID
참조)EXECUTED_SQL
(LONGTEXT NOT NULL): 최종 실행된 SQL 문장PARAMETER_VALUES
(JSON): 실행 시 사용된 최종 파라미터 값들 (JSON 형태로 저장하여 유연성 확보)EXECUTION_STATUS
(VARCHAR(50) NOT NULL): 실행 상태 (SUCCESS, FAILED 등)RESULT_DATA
(LONGTEXT): SQL 실행 결과 데이터 (필요시 저장)ERROR_MESSAGE
(TEXT): 에러 발생 시 메시지EXECUTED_AT
(TIMESTAMP DEFAULT CURRENT\_TIMESTAMP): 실행일시Spring Boot와 MyBatis(iBatis의 후속 프레임워크)를 기반으로 주요 로직 단계를 설명합니다.
개요:
사용자가 웹 UI를 통해 테스트 계획, 시나리오, 케이스, 액티비티를 정의하고, 각 단계에서 필요한 파라미터들을 설정합니다. 이 정보들은 위에서 설계한 DB 테이블에 저장됩니다.
주요 컴포넌트:
저장 예시 (MyBatis Mapper XML):
<insert id="insertTestPlan" useGeneratedKeys="true" keyProperty="planId">
INSERT INTO TEST_PLAN (PLAN_NAME, DESCRIPTION)
VALUES (#{planName}, #{description})
</insert>
<insert id="insertParameterDefinition">
INSERT INTO PARAMETER_DEFINITION (REFER_ID, REFER_TYPE, PARAM_NAME, PARAM_TYPE, PARAM_VALUE, DESCRIPTION)
VALUES (#{referId}, #{referType}, #{paramName}, #{paramType}, #{paramValue}, #{description})
</insert>
로직 흐름:
TestPlanController
가 요청을 받아 TestPlanService
호출.TestPlanService
는 TestPlanMapper
를 통해 TEST_PLAN
테이블에 데이터 삽입.TEST_SCENARIO
, TEST_CASE
, TEST_ACTIVITY
및 각 단계의 PARAMETER_DEFINITION
데이터를 저장. 특히 SQL_TEMPLATE
은 TEST_ACTIVITY
테이블에 그대로 저장됩니다.개요:
사용자가 특정 테스트 계획을 선택하여 실행을 요청하면, 해당 계획에 속한 모든 시나리오, 케이스, 활동들을 조회하여 실행 가능한 테스트 세트를 구성합니다. 이 단계에서는 실제 SQL 실행은 하지 않고, 실행 준비를 위한 데이터를 로드합니다.
주요 컴포넌트:
조회 예시 (MyBatis Mapper XML):
<resultMap id="activityWithParams" type="com.example.testtool.model.TestActivity">
<id property="activityId" column="ACTIVITY_ID"/>
<result property="activityName" column="ACTIVITY_NAME"/>
<result property="sqlTemplate" column="SQL_TEMPLATE"/>
<collection property="parameters" ofType="com.example.testtool.model.ParameterDefinition">
<id property="paramDefId" column="PARAM_DEF_ID"/>
<result property="paramName" column="PARAM_NAME"/>
<result property="paramType" column="PARAM_TYPE"/>
<result property="paramValue" column="PARAM_VALUE"/>
</collection>
</resultMap>
<select id="getActivitiesWithParametersByPlanId" resultMap="activityWithParams">
SELECT
ta.ACTIVITY_ID, ta.ACTIVITY_NAME, ta.SQL_TEMPLATE,
pd.PARAM_DEF_ID, pd.PARAM_NAME, pd.PARAM_TYPE, pd.PARAM_VALUE
FROM TEST_ACTIVITY ta
JOIN TEST_CASE tc ON ta.CASE_ID = tc.CASE_ID
JOIN TEST_SCENARIO ts ON tc.SCENARIO_ID = ts.SCENARIO_ID
JOIN TEST_PLAN tp ON ts.PLAN_ID = tp.PLAN_ID
LEFT JOIN PARAMETER_DEFINITION pd ON (pd.REFER_ID = ta.ACTIVITY_ID AND pd.REFER_TYPE = 'ACTIVITY')
OR (pd.REFER_ID = tc.CASE_ID AND pd.REFER_TYPE = 'CASE')
OR (pd.REFER_ID = ts.SCENARIO_ID AND pd.REFER_TYPE = 'SCENARIO')
OR (pd.REFER_ID = tp.PLAN_ID AND pd.REFER_TYPE = 'PLAN')
WHERE tp.PLAN_ID = #{planId}
ORDER BY ts.SCENARIO_ID, tc.CASE_ID, ta.ACTIVITY_ID, pd.REFER_TYPE DESC, pd.PARAM_NAME
</select>
로직 흐름:
PLAN_ID
로 테스트 실행 요청.TestExecutionService
는 getActivitiesWithParametersByPlanId
같은 매퍼를 호출하여 해당 계획에 속한 모든 Activity와 각 계층(Plan, Scenario, Case, Activity)에 정의된 모든 파라미터를 조회합니다.ExecutionJob
또는 TestExecution
객체 리스트를 생성합니다. 각 ExecutionJob
은 하나의 TestActivity
와 해당 Activity
에 적용될 가능성이 있는 모든 상위/자신 계층의 파라미터 정보를 포함합니다.개요:
이 단계는 가장 중요하며, 조회된 SQL 템플릿에 최종적으로 적용될 파라미터 값을 결정합니다. 우선순위는 Test Activity (Level 1) \> Test Case (Level 2) \> Test Scenario (Level 3) \> Test Plan (Level 4) 순으로 적용됩니다.
주요 컴포넌트:
로직 흐름 (ParameterResolver
의 역할):
public Map<String, Object> resolveParameters(Long planId, Long scenarioId, Long caseId, Long activityId) {
Map<String, Object> finalParameters = new HashMap<>();
// 1. Level 4: Test Plan 파라미터 로드 및 적용
loadAndApplyParameters(finalParameters, planId, "PLAN");
// 2. Level 3: Test Scenario 파라미터 로드 및 적용 (동일 이름 시 덮어쓰기)
loadAndApplyParameters(finalParameters, scenarioId, "SCENARIO");
// 3. Level 2: Test Case 파라미터 로드 및 적용 (동일 이름 시 덮어쓰기)
loadAndApplyParameters(finalParameters, caseId, "CASE");
// 4. Level 1: Test Activity 파라미터 로드 및 적용 (동일 이름 시 덮어쓰기)
loadAndApplyParameters(finalParameters, activityId, "ACTIVITY");
// 5. 시스템 파라미터 추가 (항상 최신값으로 적용)
finalParameters.put("testId", "TEST_" + System.currentTimeMillis());
finalParameters.put("scenarioId", "SCENARIO_" + System.currentTimeMillis());
// 6. 런타임 계산 파라미터 (Dynamic Parameters) 처리
// PARAM_TYPE이 'DYNAMIC'인 파라미터는 PARAM_VALUE에 정의된 SQL을 실행하여 결과값을 finalParameters에 추가
processDynamicParameters(finalParameters);
// 7. SQL 쿼리 결과값 파라미터 (SQL Query Parameters) 처리
// PARAM_TYPE이 'SQL_QUERY'인 파라미터는 PARAM_VALUE에 정의된 SQL을 실행하여 결과값을 finalParameters에 추가
// 이는 SQL 템플릿 치환 이전에 처리되어야 함.
processSqlQueryParameters(finalParameters);
return finalParameters;
}
private void loadAndApplyParameters(Map<String, Object> params, Long referId, String referType) {
if (referId == null) return;
List<ParameterDefinition> definitions = parameterDefinitionMapper.findByReferIdAndType(referId, referType);
for (ParameterDefinition def : definitions) {
// SYSTEM, CONDITIONAL, STATIC 파라미터는 여기서 직접 적용
if (def.getParamType().equals("SYSTEM") || def.getParamType().equals("STATIC") || def.getParamType().equals("CONDITIONAL")) {
params.put(def.getParamName(), def.getParamValue());
}
// DYNAMIC, SQL_QUERY 타입은 나중에 별도 처리
}
}
private void processDynamicParameters(Map<String, Object> params) {
// PARAMETER_DEFINITION 테이블에서 'DYNAMIC' 타입의 파라미터를 찾아
// PARAM_VALUE에 있는 SQL을 실행하여 결과값을 params에 추가
// 이 과정은 DataSource 및 JdbcTemplate 필요
// 예: select sysdate from dual
}
private void processSqlQueryParameters(Map<String, Object> params) {
// PARAMETER_DEFINITION 테이블에서 'SQL_QUERY' 타입의 파라미터를 찾아
// PARAM_VALUE에 있는 SQL을 실행하여 결과값을 params에 추가
// 이 결과는 해당 파라미터가 사용될 SQL 템플릿에 직접 치환될 예정
}
파라미터 처리 순서 (최종 SQL 생성 전):
$paramName
): $testId
, $scenarioId
등 시스템에서 사전에 정의된 파라미터들을 ParameterResolver
에서 생성된 최종 파라미터 맵의 값으로 치환합니다.SQL Query Parameters
) 처리: SQL Query Parameters
타입의 파라미터(PARAM_TYPE = 'SQL_QUERY'
)는 PARAM_VALUE
에 정의된 SQL을 직접 실행하여 그 결과값으로 SQL 템플릿 내의 해당 파라미터 이름을 대체합니다. 이는 SQL 문맥에 따라 다르게 처리될 수 있으므로, SQL 템플릿 치환 전에 미리 처리되어야 합니다.#{conditionalSql}
): #{ADD_CONDITION_1}
과 같이 조건부로 추가되는 SQL 블록을 처리합니다. ParameterResolver
에서 결정된 최종 파라미터 맵에 ADD_CONDITION_1
이라는 키가 존재하고 값이 비어있지 않다면 해당 값을 SQL에 삽입합니다. 값이 없거나 비어있으면 해당 블록을 제거합니다.CONDITIONAL
파라미터와 유사하게 처리될 수 있습니다. PARAM_TYPE = 'CONDITIONAL'
로 정의된 파라미터들이 finalParameters
맵에 존재하고 그 값이 유효하다면, SQL 템플릿의 WHERE 1=1
뒤에 해당 조건절을 추가합니다.개요:
파라미터 바인딩이 완료된 최종 SQL을 MariaDB에 실행하고 그 결과를 저장합니다.
주요 컴포넌트:
JdbcTemplate
: 동적으로 생성된 SQL을 실행하기에 적합합니다.JdbcTemplate
사용이 더 유연합니다.로직 흐름:
@Service
public class SqlExecutorService {
private final JdbcTemplate jdbcTemplate;
private final TestExecutionResultMapper resultMapper;
public SqlExecutorService(JdbcTemplate jdbcTemplate, TestExecutionResultMapper resultMapper) {
this.jdbcTemplate = jdbcTemplate;
this.resultMapper = resultMapper;
}
@Transactional
public void executeSqlAndSaveResult(Long activityId, String processedSql, Map<String, Object> finalParameters) {
String executionStatus = "FAILED";
String errorMessage = null;
String resultData = null;
try {
// SELECT 문인 경우 쿼리 실행 및 결과 저장
if (processedSql.trim().toUpperCase().startsWith("SELECT")) {
List<Map<String, Object>> rows = jdbcTemplate.queryForList(processedSql);
resultData = new ObjectMapper().writeValueAsString(rows); // JSON 형태로 저장
}
// INSERT, UPDATE, DELETE 등 DML 문인 경우 업데이트 갯수 저장
else {
int affectedRows = jdbcTemplate.update(processedSql);
resultData = "Affected Rows: " + affectedRows;
}
executionStatus = "SUCCESS";
} catch (Exception e) {
errorMessage = e.getMessage();
// 에러 로깅
} finally {
// 실행 결과 저장
TestExecutionResult result = new TestExecutionResult();
result.setActivityId(activityId);
result.setExecutedSql(processedSql);
result.setParameterValues(new ObjectMapper().writeValueAsString(finalParameters)); // JSON 문자열로 변환
result.setExecutionStatus(executionStatus);
result.setResultData(resultData);
result.setErrorMessage(errorMessage);
resultMapper.insertTestExecutionResult(result);
}
}
}
개요:
SQL 실행 후 결과를 TEST_EXECUTION_RESULT
테이블에 저장합니다. 이 결과는 나중에 테스트 리포트 생성 등에 활용될 수 있습니다.
주요 컴포넌트:
TEST_EXECUTION_RESULT
테이블에 데이터를 삽입합니다.저장 예시 (MyBatis Mapper XML):
<insert id="insertTestExecutionResult" useGeneratedKeys="true" keyProperty="execId">
INSERT INTO TEST_EXECUTION_RESULT (ACTIVITY_ID, EXECUTED_SQL, PARAMETER_VALUES, EXECUTION_STATUS, RESULT_DATA, ERROR_MESSAGE)
VALUES (#{activityId}, #{executedSql}, #{parameterValues}, #{executionStatus}, #{resultData}, #{errorMessage})
</insert>
SQL_QUERY
타입의 파라미터나 동적 SQL을 사용할 경우, 최대한 제한적인 권한의 DB 계정을 사용하고, 입력값에 대한 화이트리스트 방식의 검증을 강력히 적용해야 합니다.PARAM_VALUE
에 저장된 SQL을 별도의 JdbcTemplate
으로 실행하여 결과값을 받아와야 합니다. 이때, 해당 SQL 자체도 또 다른 파라미터를 포함할 수 있는지 등 복잡도를 고려해야 합니다.PARAM_VALUE
에 정의된 SQL을 실행하고, 그 결과값(단일 값으로 가정)으로 SQL 템플릿 내의 특정 ${paramName}을 대체해야 합니다. 이는 문자열 치환 방식으로 구현될 수 있습니다.TestActivity
의 SQL 실행을 비동기로 처리하여 시스템 응답성을 높일 수 있습니다 (예: Spring @Async
, CompletableFuture
).TEST_EXECUTION_RESULT
데이터를 기반으로 테스트 실행 보고서를 생성하는 기능이 추가되어야 합니다.이 설계와 로직 흐름은 제시된 요구사항을 충족하며, 안정적이고 확장 가능한 테스트 도구 시스템을 구축하는 데 도움이 될 것입니다.