<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dev_seung.log</title>
        <link>https://velog.io/</link>
        <description>MacBook이 갖고싶은 살암</description>
        <lastBuildDate>Mon, 15 Jul 2024 07:41:32 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dev_seung.log</title>
            <url>https://velog.velcdn.com/images/dev_seung/profile/ca68986a-e344-400c-b0f5-8650442df2a2/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dev_seung.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_seung" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Flask REST API 서버와 PLC 및 Redis 통합 (1)]]></title>
            <link>https://velog.io/@dev_seung/Ethernet-IP-%EC%84%A4%EC%A0%95-python</link>
            <guid>https://velog.io/@dev_seung/Ethernet-IP-%EC%84%A4%EC%A0%95-python</guid>
            <pubDate>Mon, 15 Jul 2024 07:41:32 GMT</pubDate>
            <description><![CDATA[<h4 id="flask-rest-api-서버와-plc-및-redis-통합">Flask REST API 서버와 PLC 및 Redis 통합</h4>
<p>이번 글에서는 Flask를 이용한 REST API 서버와 PLC 및 Redis와의 통합을 다루는 두 개의 코드 스니펫을 분석해 보겠다. 첫 번째 코드 스니펫은 Flask를 사용하여 EIP(Engineering Protocol) 관련 태그를 관리하는 REST API 서버를 설정하고, 두 번째 코드 스니펫은 PLC와 Redis를 이용하여 태그 데이터를 읽고 쓰는 기능을 구현한다.
<br></p>
<hr>

<p><strong>1. Flask REST API 서버 코드</strong>
Flask와 Flask-RESTx 설정:</p>
<ul>
<li>Flask 애플리케이션을 생성하고, Flask-RESTx를 사용하여 API를 구성한다.<pre><code class="language-python">from flask import Flask, request
from flask_restx import Api, Resource, fields
from taglist import process_tags, start_read, stop_read, write_data, stop_tag
</code></pre>
</li>
</ul>
<p>app = Flask(<strong>name</strong>)
api = Api(app, version=&#39;1.0&#39;, title=&#39;EIP Tag&#39;)</p>
<pre><code>&lt;br&gt;

API 네임스페이스와 모델 정의:
- /api/eip 네임스페이스를 설정하고, 데이터 모델을 정의한다.
```python
ns = api.namespace(&#39;api/eip&#39;, description=&#39;EIP operations&#39;)

memory_map_model = api.model(&#39;MemoryMap&#39;, {
    &#39;type&#39;: fields.String(description=&#39;Memory type&#39;),
    &#39;address&#39;: fields.Integer(description=&#39;Address&#39;),
    &#39;dataType&#39;: fields.String(description=&#39;Data type&#39;),
    &#39;rwType&#39;: fields.String(description=&#39;Read/Write type&#39;),
    &#39;tagArraySize&#39;: fields.Integer(description=&#39;Tag array size&#39;),
    &#39;tagArray&#39;: fields.List(fields.String, description=&#39;Tag array&#39;)
})

tag_model = api.model(&#39;Tag&#39;, {
    &#39;type&#39;: fields.String(description=&#39;Type&#39;),
    &#39;id&#39;: fields.String(description=&#39;Unique ID&#39;),
    &#39;name&#39;: fields.String(description=&#39;Alias name&#39;),
    &#39;memoryMap&#39;: fields.List(fields.Nested(memory_map_model), description=&#39;Memory map&#39;)
})

write_data_model = api.model(&#39;WriteData&#39;, {
    &#39;tags&#39;: fields.List(fields.Nested(api.model(&#39;WriteTag&#39;, {
        &#39;tag&#39;: fields.String(required=True, description=&#39;Tag to write&#39;),
        &#39;value&#39;: fields.String(required=True, description=&#39;Value to write&#39;)
    })))
})
</code></pre><br>

<p>API 엔드포인트 정의:</p>
<ul>
<li>/state: 현재 EIP 서버 상태를 조회한다.</li>
<li>/tag: 태그를 등록하고, 읽기 작업을 중지한다.</li>
<li>/read: 등록된 모든 태그를 읽기 시작하고, 읽기 작업을 중지한다.</li>
<li>/write: 태그에 데이터를 쓴다.<pre><code class="language-python">@ns.route(&#39;/state&#39;)
class EIPState(Resource):
  @ns.doc(description=&#39;Get the current state of the Python EIP server&#39;)
  def get(self):
      return {&#39;state&#39;: &#39;running&#39;}, 200
</code></pre>
</li>
</ul>
<p>@ns.route(&#39;/tag&#39;)
class Tag(Resource):
    @ns.doc(description=&#39;Register tags from the provided JSON&#39;)
    @ns.expect(tag_model)
    def post(self):
        data = request.json
        global tag_name_dict
        tag_name_dict = process_tags(data)  # process_tags 함수를 호출하여 tag_name_dict 생성
        return {&#39;results&#39;: list(tag_name_dict.keys())}, 201</p>
<pre><code>@ns.doc(description=&#39;Stop reading specified tags&#39;)
def delete(self):
    global tag_name_dict
    if not tag_name_dict:
        return {&#39;message&#39;: &#39;No tags to stop&#39;}, 400
    tag_list = list(tag_name_dict.keys())  # 모든 등록된 태그 이름들의 리스트
    results = stop_tag(tag_list)
    tag_name_dict.clear()
    return {&#39;results&#39;: results}, 200</code></pre><p>@ns.route(&#39;/read&#39;)
class Read(Resource):
    @ns.doc(description=&#39;Start reading all registered tags&#39;)
    def post(self):
        global tag_name_dict
        if not tag_name_dict:
            return {&#39;message&#39;: &#39;No tags to read&#39;}, 400
        tag_list = list(tag_name_dict.keys())  # 모든 등록된 태그 이름들의 리스트
        results = start_read(tag_list)
        return {&#39;results&#39;: results}, 201</p>
<pre><code>@ns.doc(description=&#39;Stop redis server&#39;)
def delete(self):
    # Stop reading tags
    stop_read()
    return {&#39;message&#39;: &#39;Stopped reading&#39;}, 200</code></pre><p>@ns.route(&#39;/write&#39;)
class Write(Resource):
    @ns.doc(description=&#39;Write data to tags&#39;)
    @ns.expect(write_data_model)
    def post(self):
        data = request.json
        if not data or not data.get(&#39;tags&#39;):
            return {&#39;message&#39;: &#39;No data to write&#39;}, 400
        result = write_data(data)
        return {&#39;result&#39;: result}, 201</p>
<p>if <strong>name</strong> == &#39;<strong>main</strong>&#39;:
    app.run(host=&#39;0.0.0.0&#39;, port=5000, debug=True)</p>
<p>```</p>
<br>

<p><strong>핵심 기능</strong></p>
<p>EIPState 클래스는 EIP 서버의 상태를 조회하는 엔드포인트를 제공한다.
Tag 클래스는 태그를 등록하고, 읽기 작업을 중지할 수 있는 엔드포인트를 제공한다.
Read 클래스는 등록된 태그의 읽기를 시작하고 중지하는 기능을 제공한다.
Write 클래스는 태그에 데이터를 쓰는 기능을 제공한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[실시간 카메라 영상 저장 프로그램 - Python]]></title>
            <link>https://velog.io/@dev_seung/video-save-python</link>
            <guid>https://velog.io/@dev_seung/video-save-python</guid>
            <pubDate>Fri, 10 May 2024 00:25:38 GMT</pubDate>
            <description><![CDATA[<h3 id="실시간-카메라-영상-저장-프로그램---python">실시간 카메라 영상 저장 프로그램 - Python</h3>
<p>이번 글에서는 실시간 카메라에서 byte 데이터를 받아 사용자가 지정한 프레임수와 초수만큼 영상을 저장하는 Python 프로그램에 대해 설명할 것이다. 이 프로그램은 비동기 처리를 통해 효율적으로 영상을 저장하고 관리할 수 있다. 아래에 프로그램의 주요 기능과 코드를 분석해보자.</p>
<hr>

<h4 id="주요-기능">주요 기능</h4>
<p><strong>1. 비동기 전처리 및 후처리:</strong>
영상 저장 작업 전에 전처리를 수행하고, 작업 후 후처리를 수행할 수 있다.
<strong>2. 실시간 영상 저장:</strong>
카메라에서 실시간으로 받은 byte 데이터를 영상으로 변환하여 지정된 프레임수와 초수만큼 저장할 수 있다.
<strong>3. 이전 파일 삭제:</strong>
이전에 저장된 파일 중 끝 시간이 없는 파일을 삭제할 수 있다.
<strong>4. 파일명 변경:</strong>
저장된 영상 파일의 시작 시간과 끝 시간을 포함한 파일명으로 변경할 수 있다.</p>
<hr>

<h4 id="코드-분석">코드 분석</h4>
<p><strong>1. 비동기 전처리 및 후처리</strong></p>
<p>preprocessing 메서드는 전처리를, postprocessing 메서드는 후처리를 수행한다. 각각 1초의 지연을 두어 로그를 남긴다.</p>
<pre><code class="language-python">async def preprocessing(self, *args, **kwargs):
    self._logger.info(&quot;preprocessing...&quot;)
    await asyncio.sleep(1)
    self._logger.info(&quot;preprocessing is finished&quot;)

async def postprocessing(self, *args, **kwargs):
    self._logger.info(&quot;postprocessing...&quot;)
    await asyncio.sleep(1)
    self._logger.info(&quot;postprocessing is finished&quot;)
</code></pre>
<br>

<p><strong>2. 실시간 영상 저장</strong></p>
<p>execute 메서드는 실시간으로 카메라 데이터를 받아 영상을 저장하는 메인 기능을 수행한다. input_data를 받아 각 데이터에 대해 처리하고, 영상을 저장한다.</p>
<pre><code class="language-python">async def execute(self, input_data: Dict[str, List[Any]]):
    self._logger.info(f&#39;Video.execute() starts ...&#39;)
    if input_data is None:
        self._logger.info(&quot;input_data none&quot;)
        return
    json_attribute: JsonAttribute = self.command_attribute.command.attribute
    json_node = json_attribute.json_node

    video_sec = json_node[&quot;sec&quot;]
    video_fps = json_node[&quot;fps&quot;]
    self.video_output_path = json_node[&quot;outputPath&quot;]
    timestamp = None
    snapshot_array = None
    for id, stream_data in input_data.items():
        for snapshot_data in stream_data:
            timestamp = snapshot_data[0]
            snapshot_array = snapshot_data[1]
            self._logger.info(f&#39;id: {id}, timestamp: {timestamp}, snapshot_data:&#39;)

    img = cv2.imdecode(np.frombuffer(snapshot_array, np.uint8), cv2.IMREAD_COLOR)
    size = (img.shape[1], img.shape[0])

    if self.size is not None and size != self.size:
        img = cv2.resize(img, self.size, interpolation=cv2.INTER_AREA)

    if self.out is None:
        self.start_timestamp = int(timestamp)
        dt = datetime.datetime.fromtimestamp(self.start_timestamp / 1e9)
        format_folder = dt.strftime(&quot;%Y-%m-%d&quot;)
        formatted_time = dt.strftime(&quot;%H-%M-%S-%f&quot;)[:-3]
        self.folder_name = f&quot;{self.video_output_path}/{format_folder}&quot;
        self.filename = f&quot;{formatted_time}.mp4&quot;
        self.output_path = os.path.join(self.folder_name, self.filename)
        self.size = (img.shape[1], img.shape[0])
        if not os.path.exists(self.folder_name):
            os.makedirs(self.folder_name)
        self._logger.info(f&#39;path : {self.output_path}&#39;)
        self.out = cv2.VideoWriter(self.output_path, cv2.VideoWriter_fourcc(*&#39;mp4v&#39;), video_fps, self.size)
        self.delete_previous_file_without_end_time()

    self.end_timestamp = int(timestamp)

    try:
        self.count += 1
        self._logger.debug(self.count)
        self.out.write(img)
        if self.count &gt;= (video_fps * video_sec):
            self.release()
    except Exception as e:
        self._logger.error(f&quot;Exception: {e}&quot;)
        self.release()</code></pre>
<br>

<p><strong>3. 이전 파일 삭제</strong></p>
<p>delete_previous_file_without_end_time 메서드는 저장된 파일 중 끝 시간이 없는 파일을 찾아 삭제한다.</p>
<pre><code class="language-python">def delete_previous_file_without_end_time(self):
    previous_file = None
    previous_file_path = None
    for root, _, files in os.walk(self.video_output_path):
        for file in sorted(files):
            if file.endswith(&#39;.mp4&#39;) and file &lt; self.filename:
                previous_file = file
                previous_file_path = os.path.join(root, previous_file)
            if file == self.filename:
                break

    if previous_file:
        if len(previous_file.split(&#39;_&#39;)) == 1:
            try:
                os.remove(previous_file_path)
                self._logger.info(f&#39;Removed previous file without end time: {previous_file_path}&#39;)
            except Exception as e:
                self._logger.error(f&#39;Error removing previous file: {previous_file_path}, Exception: {e}&#39;)
</code></pre>
<br>

<p><strong>4. 파일명 변경</strong></p>
<p>release 메서드는 영상 저장이 완료되면 파일명을 시작 시간과 끝 시간을 포함하도록 변경한다.</p>
<pre><code class="language-python">def release(self):
    if self.out is not None:
        self.out.release()
        if self.start_timestamp is not None and self.end_timestamp is not None:
            start_dt = datetime.datetime.fromtimestamp(self.start_timestamp / 1e9)
            end_dt = datetime.datetime.fromtimestamp(self.end_timestamp / 1e9)
            start_time = start_dt.strftime(&quot;%H-%M-%S-%f&quot;)[:-3]
            end_time = end_dt.strftime(&quot;%H-%M-%S-%f&quot;)[:-3]
            new_filename = f&quot;{start_time}_{end_time}.mp4&quot;
            new_output_path = os.path.join(self.folder_name, new_filename)
            os.rename(self.output_path, new_output_path)
            self._logger.info(f&#39;Renamed file to: {new_output_path}&#39;)

    self.out = None
    self.size = None
    self.count = 0
    self.start_timestamp = None
    self.end_timestamp = None
    self._logger.info(f&#39;Video.execute() is finished&#39;)</code></pre>
<br>


<p>이 프로그램은 실시간으로 카메라 데이터를 받아 영상을 저장하고, 사용자가 지정한 조건에 맞게 파일을 관리할 수 있게 해준다. 비동기 처리를 통해 효율성을 높이고, 파일 관리 기능을 통해 저장된 영상을 체계적으로 관리할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Elasticsearch Log 관리 프로그램 - Java (1)]]></title>
            <link>https://velog.io/@dev_seung/elasticsearch-java-log</link>
            <guid>https://velog.io/@dev_seung/elasticsearch-java-log</guid>
            <pubDate>Fri, 12 Apr 2024 01:47:53 GMT</pubDate>
            <description><![CDATA[<h2 id="elasticsearch-log-관리-프로그램---java">Elasticsearch Log 관리 프로그램 - Java</h2>
<p>이번 글에서는 Java로 작성한 Elasticsearch 로그 관리 프로그램에 대해 설명할 것 이다. 이 프로그램은 여러 기능을 제공하여 Elasticsearch 로그 데이터를 효율적으로 관리하고 검색할 수 있다. 아래에 프로그램의 주요 기능과 코드를 분석해보자.</p>
<h4 id="주요-기능">주요 기능</h4>
<p><strong>1. 로그 검색 및 페이징:</strong>
검색 조건에 맞는 로그 데이터를 Elasticsearch에서 검색하고, 페이징 처리하여 결과를 반환할 수 있다.
<strong>2. 문서 생성:</strong>
주어진 로그 데이터를 Elasticsearch에 문서로 생성할 수 있다.
<strong>3. 인덱스 설정 업데이트:</strong>
특정 인덱스의 설정을 업데이트할 수 있다.
<strong>4. 인덱스 삭제:</strong>
주어진 인덱스를 삭제할 수 있다.
<strong>5. 모든 인덱스 목록 가져오기:</strong>
Elasticsearch에 존재하는 모든 인덱스의 목록을 가져올 수 있다.</p>
<hr>


<h4 id="코드-분석">코드 분석</h4>
<p><strong>1. 로그 검색 및 페이징</strong></p>
<p>getSearchLogList 메서드는 주어진 검색 조건에 맞는 로그 데이터를 검색하고 페이징 처리하여 반환하는 기능을 수행한다. BoolQueryBuilder를 사용하여 복잡한 검색 쿼리를 구성하고, SearchSourceBuilder를 사용하여 검색 요청을 작성한다.</p>
<pre><code class="language-java">public PageDto&lt;LogsDto&gt; getSearchLogList(SearchLogDto searchLogDto) throws IOException {
    PageDto&lt;LogsDto&gt; dto = new PageDto&lt;&gt;();
    BoolQueryBuilder boolQueryBuilder = queryBuilder(searchLogDto);

    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    searchSourceBuilder.query(boolQueryBuilder);
    searchSourceBuilder.sort(Str_time, SortOrder.DESC);
    searchSourceBuilder.from((searchLogDto.getPageNum() - 1) * searchLogDto.getRowCnt());
    searchSourceBuilder.size(searchLogDto.getRowCnt());
    searchSourceBuilder.trackTotalHits(true);

    ArrayList&lt;String&gt; eqIdList = new ArrayList&lt;&gt;();
    for (Long eqId : searchLogDto.getEqId()) {
        eqIdList.add(eqId.toString() + &quot;-*&quot;);
    }

    SearchRequest searchRequest = new SearchRequest();
    String[] indices = eqIdList.stream().map(Object::toString).toArray(String[]::new);
    searchRequest.indices(indices);
    searchRequest.source(searchSourceBuilder);

    try {
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits searchHits = searchResponse.getHits();
        long totalCount = searchHits.getTotalHits().value;
        indexMaxResult(eqIdList, totalCount);

        List&lt;LogsDto&gt; logsDtoList = Arrays.stream(searchHits.getHits())
                .map(this::mapToLogsDto)
                .collect(Collectors.toList());

        dto.setContent(logsDtoList);
        dto.setTotalPages((int) Math.ceil((double) totalCount / searchLogDto.getRowCnt()));
        dto.setTotalElements(totalCount);
        dto.setFirst(searchLogDto.getPageNum() == 1);
        dto.setLast(searchLogDto.getPageNum() == dto.getTotalPages());

    } catch (IOException e) {
        logger.error(&quot;Error occurred while searching documents: {}&quot;, e.getMessage());
        return new PageDto&lt;&gt;();
    }

    return dto;
}
</code></pre>
<br>

<p>queryBuilder 메서드는 주어진 검색 조건을 기반으로 BoolQueryBuilder를 생성한다. 이를 통해 다양한 필드에 대한 조건을 유연하게 추가할 수 있다.</p>
<pre><code class="language-java">private BoolQueryBuilder queryBuilder(SearchLogDto searchLogDto) {
    BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();

    if (searchLogDto.getMessage() != null) 
        boolQueryBuilder.must(QueryBuilders.matchQuery(Str_message, searchLogDto.getMessage()).analyzer(&quot;camel_analyzer&quot;).operator(Operator.AND));

    if (searchLogDto.getWatch() != null) 
        boolQueryBuilder.must(QueryBuilders.matchQuery(Str_watch, searchLogDto.getWatch()).analyzer(&quot;camel_analyzer&quot;).operator(Operator.AND));

    if (searchLogDto.getNodeId() != null) 
        boolQueryBuilder.must(QueryBuilders.matchPhraseQuery(Str_nodeId, searchLogDto.getNodeId()));

    boolQueryBuilder.must(QueryBuilders.matchQuery(Str_level, String.join(&quot;,&quot;, searchLogDto.getLevel())));

    if (searchLogDto.getStartTime() != null &amp;&amp; searchLogDto.getEndTime() != null) {
        searchLogDto.setStartTime(setTime(searchLogDto.getStartTime()));
        searchLogDto.setEndTime(setTime(searchLogDto.getEndTime()));
        RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery(Str_time)
                .gte(searchLogDto.getStartTime())
                .lt(searchLogDto.getEndTime());
        boolQueryBuilder.must(rangeQuery);
    }

    return boolQueryBuilder;
}</code></pre>
<br>

<p>mapToLogsDto 메서드는 Elasticsearch의 검색 결과를 LogsDto 객체로 매핑한다. 검색된 각 문서의 소스 데이터를 LogsDto로 변환하고, 문서 ID를 설정한다.</p>
<pre><code class="language-java">private LogsDto mapToLogsDto(SearchHit hit) {
    String sourceAsString = hit.getSourceAsString();
    String id = hit.getId(); // Document ID 추출
    if (sourceAsString != null) {
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            LogsDto logsDto = objectMapper.readValue(sourceAsString, LogsDto.class);
            logsDto.setId(id); // Document ID 설정
            return logsDto;
        } catch (JsonProcessingException e) {
            logger.error(&quot;Error while mapping JSON to LogsDto: {}&quot;, e.getMessage());
            return null;
        }
    }
    return null;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[reactjs env 동적 사용]]></title>
            <link>https://velog.io/@dev_seung/reactjs-env-%EB%8F%99%EC%A0%81-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@dev_seung/reactjs-env-%EB%8F%99%EC%A0%81-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Fri, 05 Apr 2024 02:30:41 GMT</pubDate>
            <description><![CDATA[<h2 id="docker로-동적-환경-설정-관리하기">Docker로 동적 환경 설정 관리하기</h2>
<p>서버 URL 같은 환경 변수를 자주 바꿔야 할 때, 매번 Docker 이미지를 새로 만들면 너무 비효율적이어서 웹 애플리케이션은 용량도 크고, 자주 이미지를 재생성하는 건 낭비가 클 수밖에 없었다. 그래서 Docker 이미지와는 별도로 설정 파일을 동적으로 생성해서 사용하는 방법을 적용했다</p>
<p><strong>구성 내용</strong></p>
<ol>
<li>generate-config.js: 환경 변수에서 값을 읽어와서 config.js 파일을 생성하는 스크립트</li>
</ol>
<pre><code class="language-js">// generate-config.js
const fs = require(&#39;fs&#39;);

// 환경 변수에서 값을 읽어와서 config 파일 생성
const configContent = `
const config = {
  apiUrl: process.env.REACT_APP_BASE_URL || &#39;http://localhost:8088/api&#39;
};

module.exports = config;
`;

fs.writeFileSync(&#39;./config.js&#39;, configContent);
</code></pre>
<p>이 스크립트는 환경 변수에서 값을 읽어와 config.js라는 설정 파일을 만들어. 기본값은 로컬 개발 환경에 맞춰져 있음</p>
<ol start="2">
<li>Dockerfile: Docker 이미지를 만들 때, generate-config.js를 실행해서 동적 설정 파일을 생성</li>
</ol>
<pre><code class="language-dockerfile"># Use an official Node.js runtime as a parent image
FROM node:20.10.0-alpine

# Set the working directory in the container
WORKDIR /app

# Copy package.json and package-lock.json
COPY package.json package-lock.json ./

# Set npm configurations
RUN npm config set 

# Install the latest version of npm globally
RUN npm install --location=global npm@latest

# Install build dependencies
RUN apk add --no-cache python3 make g++

# Install dependencies
RUN npm ci

# Copy the rest of the application code
COPY . .

# npm 스크립트 실행
RUN npm run generate-config

# npm 스크립트 실행을 위한 환경 변수 설정
ARG REACT_APP_BASE_URL

ENV REACT_APP_BASE_URL=$REACT_APP_BASE_URL

# 3008번 포트 노출
EXPOSE 3008

# npm start 스크립트 실행
CMD [&quot;npm&quot;, &quot;start&quot;]
</code></pre>
<p>이 Dockerfile은 generate-config.js를 실행해서 설정 파일을 동적으로 생성해. 이 설정 파일은 Docker 이미지 빌드 시점에 생성되고, 설정된 환경 변수에 따라 내용이 달라져.
<br></p>
<hr>

<p><strong>장점</strong>
동적 설정: 환경 변수를 Docker 이미지와는 독립적으로 관리할 수 있어서, 이미지를 새로 만들지 않고도 설정을 쉽게 바꿀 수 있다.
효율적인 빌드: 설정 파일을 동적으로 생성하니까, 빈번한 이미지 재생성 없이도 변경 사항을 반영할 수 있다.
유연성: Docker Compose에서 설정을 쉽게 바꿀 수 있어서, 다양한 환경에서 유연하게 대응할 수 있다.
이렇게 하면 Docker 환경에서 설정 관리가 훨씬 더 효율적이고 유연해졌다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[elasticsearch docker elk 설치 및 구성]]></title>
            <link>https://velog.io/@dev_seung/es-adaptor-%EA%B5%AC%EC%84%B1</link>
            <guid>https://velog.io/@dev_seung/es-adaptor-%EA%B5%AC%EC%84%B1</guid>
            <pubDate>Thu, 14 Mar 2024 07:31:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>기본 전제 조건</strong></p>
</blockquote>
<ul>
<li>docker 설치</li>
<li>java 설치</li>
</ul>
<h3 id="elk-설치-및-구성">elk 설치 및 구성</h3>
<h4 id="1-docker-elk-다운로드">1. docker-elk 다운로드</h4>
<p>-
<a href="https://github.com/deviantony/docker-elk">https://github.com/deviantony/docker-elk</a>
위 경로에서 git clone 해오게되면 아래처럼 폴더 구성이 생긴다.</p>
<pre><code class="language-bash">git clone https://github.com/deviantony/docker-elk.git</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_seung/post/6825688e-72f4-4738-8c57-1ff2f59973b3/image.png" alt=""></p>
<h4 id="2-docker-composeyml-구성">2. docker-compose.yml 구성</h4>
<p>다운로드 해온 경로에 &#39;docker-compose.yml&#39; 파일이 존재할텐데 해당 파일을 열면 기본적으로 작성되어져있다.
기본 구성대로 설치하려면 이대로 진행하면 되고, 이미 구성된 docker에 추가로 작성하려면 기본 구성에 컨테이너 내용만 추가로 작성해주면된다.</p>
<p>추가로 redis를 통해 log를 받아올거기 때문에 redis를 추가로 작성해준다.
아래는 실제 docker-compose 내용으로 기본 구성된 내용과 약간 다를 수 있다.</p>
<pre><code class="language-yml">version: &#39;1.0&#39;
services:
  redis:
    container_name: redis
    image: redis:7-alpine
    restart: unless-stopped
    hostname: redis
    ports:
      - 6379:6379
    volumes:
      - ./redis/data:/data
    labels:
      - &quot;name=redis&quot;
      - &quot;mode=standalone&quot;
    networks:
      - default

  elasticsearch:
    container_name: elasticsearch
    image: elasticsearch:8.3.2
    volumes:
      - ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro,Z
      - elasticsearch:/usr/share/elasticsearch/data:Z
    ports:
      - 9200:9200
      - 9300:9300
    environment:
      node.name: elasticsearch
      ES_JAVA_OPTS: -Xms512m -Xmx512m
      ELASTIC_PASSWORD: changeme
      discovery.type: single-node
    networks:
      - default
    restart: unless-stopped

  logstash:
    container_name: logstash
    image: logstash:8.3.2
    volumes:
      - ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml:ro,Z
      - ./logstash/pipeline:/usr/share/logstash/pipeline:ro,Z
    ports:
      - 5044:5044
      - 50000:50000/tcp
      - 50000:50000/udp
      - 9600:9600
    environment:
      LS_JAVA_OPTS: -Xms256m -Xmx256m
      LOGSTASH_INTERNAL_PASSWORD: changeme
    networks:
      - default
    depends_on:
      - elasticsearch
    restart: unless-stopped

  kibana:
    container_name: kibana
    image: kibana:8.3.2
    volumes:
      - ./kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml:ro,Z
    ports:
      - 5601:5601
    environment:
      KIBANA_SYSTEM_PASSWORD: changeme
    networks:
      - default
    depends_on:
      - elasticsearch
    restart: unless-stopped

volumes:
  setup:
  elasticsearch:

networks:
  default:
</code></pre>
<h4 id="3-elasticsearchyml-수정">3. elasticsearch.yml 수정</h4>
<ul>
<li>elasitcsearch &gt; config &gt; elasticsearch.yml
해당 경로에 elasitcsearch.yml 파일을 수정해준다. </li>
</ul>
<blockquote>
<p>xpack.license.self_generated.type: <strong>basic</strong>
기본 값으로 trial로 작성되어 있기 때문에 basic으로 수정해준다.</p>
</blockquote>
<pre><code class="language-yml">---
## Default Elasticsearch configuration from Elasticsearch base image.
## https://github.com/elastic/elasticsearch/blob/main/distribution/docker/src/docker/config/elasticsearch.yml
#
cluster.name: docker-cluster
network.host: 0.0.0.0

## X-Pack settings
## see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-settings.html
#
xpack.license.self_generated.type: basic
xpack.security:
  enabled: true</code></pre>
<h4 id="4-logstashconf-수정">4. logstash.conf 수정</h4>
<ul>
<li>logstash &gt; pipline &gt; logstash.conf
해당 경로에 logstash.conf 파일을 수정해준다. </li>
</ul>
<pre><code class="language-bash">input {
    redis { 
            host =&gt; &quot;redis&quot;
            port =&gt; 6379
            password =&gt; &#39;1234&#39;
            codec =&gt; &quot;json&quot;
            data_type =&gt; &quot;list&quot;
            key =&gt; &quot;logstash&quot;
    }
}

## Add your filters / logstash plugins configuration here

output {
    elasticsearch {
        hosts =&gt; &quot;elasticsearch:9200&quot;
        index =&gt; &quot;%{type}-%{+YYYY.MM.dd}&quot;
        user =&gt; &quot;logstash_internal&quot;
        password =&gt; &quot;changeme&quot;
    }
}</code></pre>
<h4 id="5docker-실행">5.docker 실행</h4>
<pre><code class="language-bash">sudo docker-compose up --build -d</code></pre>
<h4 id="6-kibana-접속">6. kibana 접속</h4>
<p>docker가 정상적으로 빌드되었다면 localhost:5601 접속 시 (혹은 해당 host로) 아래와 같은 로그인 화면이 뜨면 정상적으로 접속된 것이다.
<img src="https://velog.velcdn.com/images/dev_seung/post/8c841e86-f016-4f8a-a606-13cc55d08555/image.png" alt=""></p>
<p>혹시 &#39;Kibana server is not ready yet.&#39; 해당 에러가 발생하면 처음부터 다시 빌드한 뒤 아래 명령어를 추가적으로 입력해보자</p>
<pre><code class="language-bash">docker-compose up setup</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring MultipartFile to byte[] 등록 및 조회]]></title>
            <link>https://velog.io/@dev_seung/MultipartFile-to-byte</link>
            <guid>https://velog.io/@dev_seung/MultipartFile-to-byte</guid>
            <pubDate>Wed, 14 Feb 2024 06:26:57 GMT</pubDate>
            <description><![CDATA[<p>이번엔 Spring에서 이미지 파일을 byte[] 변환하여 Porstgres에 bytea 타입으로 데이터를 삽입하는 방법에 대해 설명해보자.</p>
<h3 id="사용-객체">사용 객체</h3>
<blockquote>
<p><strong>ImageFile.java</strong> :: Entity
postgres에 설계한 DB tbl 내용을 반영한 Entity 작성</p>
</blockquote>
<pre><code class="language-java">@Entity
@Table(name = &quot;image_file&quot;)
@EntityListeners(AuditingEntityListener.class)
@Setter
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ImageFile extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;id&quot;, nullable = false)
    private Long id;
    @Column(name = &quot;file&quot;)
    private byte[] file;
 }</code></pre>
<blockquote>
<p><strong>ImageFileDto.java</strong> :: DTO
객체를 주고받을 DTO 작성</p>
</blockquote>
<pre><code class="language-java">@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
public class ImageFileDto {
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private MultipartFile file;
}</code></pre>
<br>
<hr>
<br>

<h3 id="등록-multipartfile-to-byte">등록 (MultipartFile to byte[])</h3>
<blockquote>
<p><strong>ImageFileController.java</strong></p>
</blockquote>
<pre><code class="language-java">@Tag(name = &quot;file&quot;)
@PostMapping(value = &quot;/file&quot;, consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity&lt;Integer&gt; registerImageFile(@ModelAttribute ImageFileDto dto) throws IOException {
        imageFileService.registerImageFile(dto);
        return ResponseEntity.ok().body(1);
}</code></pre>
<p>기본 제공되는 DATA</p>
<pre><code class="language-java">public static final String MULTIPART_FORM_DATA_VALUE = &quot;multipart/form-data&quot;;</code></pre>
<blockquote>
<p><strong>ImageFileService.java</strong></p>
</blockquote>
<pre><code class="language-java">@Transactional(rollbackFor = Exception.class)
public void registerImageFile(ImageFileDto dto) throws IOException {
    MultipartFile multipartFile = dto.getFile();
    ImageFile imageFile = ImageFile.builder()
                    .file(multipartFile.getBytes())
                    .build();
            imageFileRepository.save(imageFile);
}</code></pre>
<p>ModelAttribute로 전달받은 Dto 객체 중 File 값을 MultipartFile로 받아주고, 해당 값의 Bytes 값을 ImageFile.java(Entity)에 Build 해준다.
Repository에 Save하면 DB에 정상적으로 저장되는 것을 확인할 수 있다.
Save는 Java에서 제공되는 메소드
<br></p>
<hr>
<br>

<h3 id="조회-byte-to-multipartfile">조회 (byte[] to MultipartFile)</h3>
<blockquote>
<p><strong>ImageFileController.java</strong></p>
</blockquote>
<pre><code class="language-java">@Tag(name = &quot;file&quot;)
@GetMapping(value = &quot;/file/{fileId}/image&quot;, produces = {MediaType.IMAGE_JPEG_VALUE})
public ResponseEntity&lt;byte[]&gt; getImageFileList(@NotNull @PathVariable Long fileId) {
    return ResponseEntity.ok().body(imageFileService.getImageFileList(fileId));
}</code></pre>
<blockquote>
<p><strong>ImageFileService.java</strong></p>
</blockquote>
<pre><code class="language-java">@Transactional(readOnly = true)
public byte[] getImageFileList(Long fileId) {
    ImageFile imageFile = ImageFileRepository.findByFile(fileId);
    return imageFile.getFile();
}</code></pre>
<p>DB에서 조회된 ImageFile 객체의 file 값 byte Array 로 반환</p>
<blockquote>
<p><strong>ImageFileRepository.java</strong></p>
</blockquote>
<pre><code class="language-java">public interface ImageFileRepository extends JpaRepository&lt;ImageFile, Long&gt; {
    @Query(value = &quot;select n from ImageFile n where n.id =:fileId&quot;)
    ImageFile findByFile(Long fileId);
}</code></pre>
<p>ImageFile 테이블에서 id 값을 조회하여 ImageFile 객체를 반환</p>
<br>
<hr>
<br>

<h3 id="결론">결론</h3>
<p>이렇게 코드로 정리하고나니 별거 아닌 것처럼 보이는데 파일 업로드 및 변환, 반환이 정상적으로 되지 않아 여러방면으로 삽질한 결과이다.
아래 이미지는 swagger에서 동작했을 때 모습으로 정상 동작하는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/dev_seung/post/2bcfd69f-5651-47c2-9c42-f576e799c5dd/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RPA 및 UiPath 개발 시 꿀팁]]></title>
            <link>https://velog.io/@dev_seung/RPA-%EB%B0%8F-UiPath-%EA%B0%9C%EB%B0%9C-%EC%8B%9C-%EA%BF%80%ED%8C%81</link>
            <guid>https://velog.io/@dev_seung/RPA-%EB%B0%8F-UiPath-%EA%B0%9C%EB%B0%9C-%EC%8B%9C-%EA%BF%80%ED%8C%81</guid>
            <pubDate>Fri, 24 Nov 2023 08:48:34 GMT</pubDate>
            <description><![CDATA[<p>*경험에서 비롯된 개인적인 의견이므로 정답은 아닐 수 있음</p>
<h3 id="1-설치-경로">1. 설치 경로</h3>
<ul>
<li>UiPath Studio 설치 시 설치 파일을 들고다니기에 용량 이슈나 여러가지면에서 부적합할 때 설치할 PC의 네트워크 연결이 되어있다면 설치 경로로 손쉽게 다운로드 가능<blockquote>
<p><a href="https://download.uipath.com/versions/21.10.10/UiPathStudio.msi">https://download.uipath.com/versions/21.10.10/UiPathStudio.msi</a></p>
</blockquote>
</li>
</ul>
<br>

<h3 id="2-셀렉터는-유효하나-클릭이-정상적으로-안될-때">2. 셀렉터는 유효하나 클릭이 정상적으로 안될 때</h3>
<ul>
<li>해당 경우는 경험 상 이러한 경우에 해결이 된 적이 있어 적어본다.
여기서 설명하는 경우는<blockquote>
<ul>
<li>클릭 액티비티가 2개 연속으로 있는 경우 </li>
<li>첫번째 클릭은 정상 동작한 것으로 눈에 확인</li>
<li>두번째 셀렉터에서 클릭을 못하는데, 실질적인 셀렉터 유효성은 유효한 것으로 확인</li>
</ul>
</blockquote>
</li>
</ul>
<p>이러한 경우이다.</p>
<p>이와 같은 경우에 클릭 액티비티를 새로 써도 셀렉터를 변경해도 잘안된다면 첫번째 클릭 액티비티의 속성 중 Simulate Click 옵션이 True인 지 확인해본다.
만약 True일 경우 기본 값인 False도 변경하여 해결이 되었다.</p>
<p>예상하기엔 Simulate Click이 활성화되면 속도면이나 화면상에서도 더 나은 옵션이라고 생각했는데, 그에 별도로 화면 상에서 클릭을 정상적으로 동작한 것으로 보이나 UiPath Click에서 정상적으로  Click이 되었다고 판단하지 않고 어디서 무언가 걸려있는 상태(?)로 계속해서 첫번째 클릭할 곳을 찾는 것으로 생각된다. 그래서 두번째 클릭 셀렉터가 유효해도 어딘가 걸려있는 첫번째 셀렉터 덕분에 정상적으로 클릭을 못하는 것으로 보인다.</p>
<br>

<h3 id="3-라이선스-정보-및-비활성화">3. 라이선스 정보 및 비활성화</h3>
<ul>
<li>Studio 라이선스 갱신, 제거, 업데이트 등 다양한 작업이 필요한 경우 명령어로 손쉽게 확인할 수 있다.
Studio 특성 상 라이선스 키의 값을 확인한다거나 제거하는 등의 작업을 별도로 할 수 있는 부분이 없어 이 방법을 쓰면 간단하게 사용할 수 있다. 또한 PC를 변경한다던지 고유한 라이선스 키 값을 다른 로봇으로 옮겨야 한다던지의 같은 상황이라면 반드시 이미 활성화 시킨 라이선스 키 값을 끊어줘야지만 사용이 가능하다. (그냥 무턱대고 Studio 삭제하는 방법X)</li>
</ul>
<blockquote>
<p>UiPath 설치 경로로 이동 (커뮤니티 버전과 트라이얼 버전 경로 상이)</p>
</blockquote>
<pre><code>cd c:\program files\uipath\studio</code></pre><p>라이선스 정보 </p>
<pre><code>uipath.licensetool.exe info</code></pre><p>라이선스 비활성화</p>
<pre><code>uipath.licensetool.exe deactivate</code></pre><p>라이선스 업데이트</p>
<pre><code>uipath.licensetool.exe update</code></pre><h3 id="4-원격-디버깅">4. 원격 디버깅</h3>
<h3 id="5-패키지-업데이트">5. 패키지 업데이트</h3>
<h3 id="6-외국-포럼-이용">6. 외국 포럼 이용</h3>
<h3 id="7-자주-사용-링큐--리스트화-반복문비교">7. 자주 사용 링큐- 리스트화, 반복문비교</h3>
<pre><code>dt_test.AsEnumerable.Where(Function(row) row(1).Equals(시트명item)).Select(Function(row) row(2).ToString).ToList</code></pre><pre><code>dt_test.AsEnumerable.Where(Function(row) row(1).Equals(시트명item)).Select(Function(row) row(2).ToString).FirstOrDefault</code></pre><pre><code>io_dt_결과.AsEnumerable.Where(Function(row) row(&quot;전표번호&quot;).Equals(전표번호item)).Select(Function(row) CDbl(row(&quot;USD&quot;).ToString)).ToArray().Sum</code></pre><h3 id="8-정규식-활용">8. 정규식 활용</h3>
<pre><code>// 숫자만 추출
body = Regex.Replace(_body, &quot;[^0-9]&quot;, &quot;&quot;)
// 영문자만 추출
body = Regex.Replace(_body, &quot;[^a-zA-Z]&quot;, &quot;&quot;)
// 한글만 추출
body = Regex.Replace(_body, &quot;[^가-힣]&quot;, &quot;&quot;)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[C# PDF 병합 북마크 유지]]></title>
            <link>https://velog.io/@dev_seung/%EB%B3%91%ED%95%A9-%EB%B6%81%EB%A7%88%ED%81%AC-%EC%9C%A0%EC%A7%80</link>
            <guid>https://velog.io/@dev_seung/%EB%B3%91%ED%95%A9-%EB%B6%81%EB%A7%88%ED%81%AC-%EC%9C%A0%EC%A7%80</guid>
            <pubDate>Thu, 26 Oct 2023 08:21:45 GMT</pubDate>
            <description><![CDATA[<h3 id="pdf-병합-시-북마크-추가">PDF 병합 시 북마크 추가</h3>
<p>여러개의 파일을 병합 시에 각 파일의 파일명을 기준으로 북마크를 추가하여 병합하는 로직</p>
<pre><code class="language-cs">PdfDocument outputDocument = new PdfDocument(); // 출력 문서 초기화
PdfDocument inputDocument = null; // 입력 문서 초기화
PdfPage page = null; // page 변수 초기화
PdfOutline outline = null; // outline 변수 초기화
int pageCount = 0; // pageCount 변수 초기화
int fileCount = 0; // fileCount 변수 초기화
string[] files = in_arr_병합파일; // 병합파일 배열 선언

foreach (string file in files)     // 파일 리스트 담은 배열 반복
{
    inputDocument = PdfReader.Open(file, PdfDocumentOpenMode.Import); // 파일 열기
     int count = inputDocument.PageCount; // 연 파일 페이지 수 추출

    for (int idx = 0; idx &lt; count; idx++) // idx=0 부터 count 까지 반복 (count = 추출한 페이지 수)
    {
        page = inputDocument.Pages[idx]; // page는 연 파일의 idx 페이지
        outputDocument.AddPage(page); // 출력 파일에 해당 page 추가
        page = outputDocument.Pages[pageCount]; // page는 출력 파일의 pageCount 페이지

        if(idx == 0) { // 현재 파일의 첫번째일 경우
            outline = outputDocument.Outlines.Add(Path.GetFileNameWithoutExtension(file), page); // 북마크 추가
         }

        pageCount = pageCount + 1; // pageCount 증가
    }

  fileCount = fileCount + 1; // fileCount 증가
 }

outputDocument.Save(in_str_저장경로); // 출력 문서 저장</code></pre>
<br>

<h3 id="pdf-병합-시-기존-북마크-유지">PDF 병합 시 기존 북마크 유지</h3>
<p>여러개의 파일을 병합 시에 기존에 갖고있던 북마크를 유지하여 병합하는 로직
<em>*북마크 상위, 하위의 트리구조까지 유지</em></p>
<pre><code class="language-cs">PdfDocument outputDocument = new PdfDocument(); // 출력 문서 초기화
PdfDocument inputDocument = null; // 입력 문서 초기화
PdfPage page = null; // page 변수 초기화
PdfOutline outline = null; // outline 변수 초기화
PdfOutlineCollection outlines = null; // outlines 변수 초기화
int pageCount = 0; // pageCount 변수 초기화
int fileCount = 0; // fileCount 변수 초기화
string[] files = in_arr_병합파일; // 병합파일 배열 선언

foreach(string file in files) // 파일 리스트 담은 배열 반복
{
    inputDocument = PdfReader.Open(file, PdfDocumentOpenMode.Import); // 파일 열기
    int count = inputDocument.PageCount; // 연 파일 페이지 수 추출
    outlines = inputDocument.Outlines; // 연 파일의 북마크 리스트 담기 (상위)

    for (int idx = 0; idx &lt; count; idx++) // idx=0 부터 count 까지 반복 (count = 추출한 페이지 수)
    {
        page = inputDocument.Pages[idx]; // page는 연 파일의 idx 페이지
        outputDocument.AddPage(page); // 출력 파일에 해당 page 추가
        page = outputDocument.Pages[pageCount]; // page는 출력 파일의 pageCount 페이지

        foreach(PdfOutline parentOutline in outlines) // 연 파일의 상위 북마크 리스트 반복
        {
            page = inputDocument.Pages[idx]; // page는 연 파일의 idx 페이지
            if (parentOutline.DestinationPage == page) // 현재 북마크와 연 파일의 북마크가 일치할 경우
            {
                page = outputDocument.Pages[pageCount]; // page는 출력 파일의 pageCount 페이지
                outline = outputDocument.Outlines.Add(parentOutline.Title, page, true); // 상위 북마크 추가
            }

            foreach(PdfOutline childOutline in parentOutline.Outlines) { // 현재의 상위 북마크의 하위 북마크 리스트 반복
                page = inputDocument.Pages[idx]; // page는 연 파일의 idx 페이지
                if (childOutline.DestinationPage == page) // 현재 북마크와 연 파일의 북마크가 일치할 경우
                {
                    page = outputDocument.Pages[pageCount]; // page는 출력 파일의 pageCount 페이지
                    outline.Outlines.Add(childOutline.Title, page, true); // 하위 북마크 추가
                }
            }

        }

        pageCount = pageCount + 1; // pageCount 증가

    }

    fileCount = fileCount + 1; // fileCount 증가

}

outputDocument.Save(in_str_저장경로); // 출력 문서 저장</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[C# PDF 페이지 지정하여 분할]]></title>
            <link>https://velog.io/@dev_seung/%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%A7%80%EC%A0%95%ED%95%98%EC%97%AC-%EB%B6%84%ED%95%A0</link>
            <guid>https://velog.io/@dev_seung/%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%A7%80%EC%A0%95%ED%95%98%EC%97%AC-%EB%B6%84%ED%95%A0</guid>
            <pubDate>Thu, 26 Oct 2023 01:20:15 GMT</pubDate>
            <description><![CDATA[<h3 id="페이지-지정하여-분할">페이지 지정하여 분할</h3>
<p>여러 페이지의 PDF에서 일정한 페이지 규칙이 있을 시 페이지 값을 입력하여 분할하는 로직</p>
<pre><code class="language-cs">PdfDocument outputDocument = new PdfDocument(); // 출력 문서 초기화
PdfDocument inputDocument = null; // 입력 문서 초기화
PdfPage page = null; // page 변수 초기화

inputDocument = PdfReader.Open(in_str_파일경로, PdfDocumentOpenMode.Import); // 파일 열기
in_int_시작페이지 = in_int_시작페이지-1; // 0부터 시작해서 -1해줌

for(int idx=in_int_시작페이지; idx&lt;in_int_종료페이지; idx++)
{
  page = inputDocument.Pages[idx]; 
      // 문서에 페이지 추가
    outputDocument.Pages.Add(page);
      // PDF로 저장 
    outputDocument.Save(in_str_저장경로);
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[C# PDF 병합]]></title>
            <link>https://velog.io/@dev_seung/C-PDF-%EB%B3%91%ED%95%A9</link>
            <guid>https://velog.io/@dev_seung/C-PDF-%EB%B3%91%ED%95%A9</guid>
            <pubDate>Thu, 12 Oct 2023 01:21:07 GMT</pubDate>
            <description><![CDATA[<p>여러 PDF 파일을 병합할 때 사용 코드이다.</p>
<p>이전 글에 UiPath Invoke Code를 이용한 PDF 라이브러리 사용법이 있으니 생략하고 작성한다.</p>
<p>인수 값으로 병합 파일 배열과 저장 경로 문자열 값을 넣어준다.</p>
<h3 id="pdf-병합">PDF 병합</h3>
<pre><code class="language-cs">PdfDocument outputDocument = new PdfDocument(); // 출력 문서 초기화
PdfDocument inputDocument = null; // 입력 문서 초기화
PdfPage page = null; // page 변수 초기화
int pageCount = 0; // pageCount 변수 초기화
int fileCount = 0; // fileCount 변수 초기화
string[] files = in_arr_병합파일; // 병합파일 배열 선언

   foreach (string file in files)    // 파일 리스트 담은 배열 반복
   {
      inputDocument = PdfReader.Open(file, PdfDocumentOpenMode.Import); // 파일 열기
       int count = inputDocument.PageCount; // 연 파일 페이지 수 추출

      for (int idx = 0; idx &lt; count; idx++) // idx=0 부터 count 까지 반복 (count = 추출한 페이지 수)
       {
          page = inputDocument.Pages[idx]; // page는 연 파일의 idx 페이지
          outputDocument.AddPage(page); // 출력 파일에 해당 page 추가
          page = outputDocument.Pages[pageCount]; // page는 출력 파일의 pageCount 페이지

         pageCount = pageCount + 1; // pageCount 증가
       }
      fileCount = fileCount + 1; // fileCount 증가

    }
   outputDocument.Save(in_str_저장경로); // 출력 문서 저장
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[C# 다중 Excel 데이터 Copy & Paste]]></title>
            <link>https://velog.io/@dev_seung/C-%EB%8B%A4%EC%A4%91-Excel-%EB%8D%B0%EC%9D%B4%ED%84%B0-Copy-Paste</link>
            <guid>https://velog.io/@dev_seung/C-%EB%8B%A4%EC%A4%91-Excel-%EB%8D%B0%EC%9D%B4%ED%84%B0-Copy-Paste</guid>
            <pubDate>Tue, 26 Sep 2023 07:14:41 GMT</pubDate>
            <description><![CDATA[<h3 id="두-개의-excel-파일에서-데이터-copy--paste">두 개의 Excel 파일에서 데이터 Copy &amp; Paste</h3>
<p>해당 코드는 &#39;test1.xlsx&#39;, &#39;test2.xlsx&#39; 이라는 파일 2개가 있을 경우
&#39;test1.xlsx&#39;의 데이터를 읽어와 일부분의 데이터를 &#39;test2.xlsx&#39; 파일 내 지정 위치에 삽입하도록 작성한 코드이다.</p>
<p>해당 내용은 기존 UiPath Activity로도 활용이 가능하나
코드 사용 시 엑셀 폼이나 양식 형식 등이 그대로 복사된다는 점이 다르다.
(Cell 값 수식도 그대로 복사됨)</p>
<pre><code class="language-cs">Microsoft.Office.Interop.Excel.Workbook wb = null;
Microsoft.Office.Interop.Excel.Workbook wb2 = null;
Microsoft.Office.Interop.Excel.Worksheet ws = null;
Microsoft.Office.Interop.Excel.Worksheet ws2 = null;
Microsoft.Office.Interop.Excel.Range rg = null;

Application excelapp = new Application();
wb = excelapp.Application.Workbooks.Open(Filename: @&quot;C:\Users\test\Desktop\test1.xlsx&quot;); // 데이터 가져올 파일
wb2 = excelapp.Application.Workbooks.Open(Filename: @&quot;C:\Users\test\Desktop\test2.xlsx&quot;); // 데이터 입력할 파일

ws = (Microsoft.Office.Interop.Excel.Worksheet)wb.Worksheets[1]; 
ws2 = (Microsoft.Office.Interop.Excel.Worksheet)wb2.Worksheets[1];

rg = ws.Range[in_CopyRange,ws.Cells[ws.UsedRange.Rows.Count, ws.UsedRange.Columns.Count] ]; // 데이터 가져올 범위
int row = rg.Rows.Count; // 가져온 데이터의 Row 수
rg.Copy();

rg = ws2.Range[in_PasteRange]; // 데이터 삽입할 셀 위치
rg.Insert(Microsoft.Office.Interop.Excel.XlDirection.xlDown); // 데이터 아래로 삽입

rg = ws2.Range[ ws2.Cells[row + 3 + 1, 1], ws2.Cells[row + 3 + 1, ws.UsedRange.Columns.Count] ]; 
// 데이터 삽입한 아래 열 위치 ( 삽입한 데이터까지의 Row 수 : [가져온 데이터 Row 수 + 작성할 시트의 삽입할 위치 위에 Row 수 ] + 하나 아래 열 : [1] )
rg.Delete(Microsoft.Office.Interop.Excel.XlDirection.xlUp);

rg = ws2.Range[&quot;A3&quot;, ws2.Cells[3, ws.UsedRange.Columns.Count]]; // 데이터 삽입한 위 열 삭제 (ex. 5열에 삽입 시 4열 삭제)
//rg.Delete(Microsoft.Office.Interop.Excel.XlDirection.xlUp);

wb2.Save();
wb.Save();
wb.Close();
wb2.Close();
excelapp.Quit();</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[C# PDFsharp UiPath 사용하기]]></title>
            <link>https://velog.io/@dev_seung/C-Code-in-PDF-UiPath-Invoke-Code</link>
            <guid>https://velog.io/@dev_seung/C-Code-in-PDF-UiPath-Invoke-Code</guid>
            <pubDate>Tue, 26 Sep 2023 05:54:14 GMT</pubDate>
            <description><![CDATA[<p>UiPath 자체 액티비티 중 PDF 관련 액티비티가 있는데, 액티비티가 정상적으로 작동하지 않아 C# Code로 작성해보았다.</p>
<p>사실 이런 PDF 병합 로직은 구글링하면 충분히 찾을 수 있는 부분인데
이걸 UiPath에서 Invoke Code로 사용하려니 몇가지 문제들이 있다.</p>
<hr>

<h3 id="💡-uipath-invoke-code-pdf-라이브러리-사용법">💡 UiPath Invoke Code PDF 라이브러리 사용법</h3>
<p>여기서 사용할 PDF 라이브러리는 &#39;pdfsharp&#39; 이다.
해당 라이브러리가 예제도 많고, 테스트했을 때 가장 안정적으로 수행이 되었다.</p>
<ol>
<li>pdfsharp 패키지 다운로드 (공식 사이트에 예제 및 다운로드 가능)</li>
<li>UiPath Studio Windows-Legacy 로 선택하여 프로세스 생성
<img src="https://velog.velcdn.com/images/dev_seung/post/de805f30-dc51-444b-89d9-0f73872771d6/image.png" alt=""></li>
<li>다운로드 받은 패키지 경로 UiPath Package 의존성에 추가</li>
<li>생성된 PDFsharp 패키지 다운로드
<img src="https://velog.velcdn.com/images/dev_seung/post/3099bdd3-0c08-4299-afd7-5cf44f9e8ac9/image.png" alt=""></li>
<li>namespaces Import 하기
<img src="https://velog.velcdn.com/images/dev_seung/post/a0824ba5-0b24-49da-a7ce-5bdd68a1863e/image.png" alt=""></li>
<li>Invoke Code 액티비티 사용 (언어를 CS로 변경)</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[C# 다국어 날짜 형식 변환]]></title>
            <link>https://velog.io/@dev_seung/C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EB%82%A0%EC%A7%9C-%ED%98%95%EC%8B%9D-%EB%B3%80%ED%99%98</link>
            <guid>https://velog.io/@dev_seung/C-%EB%8B%A4%EA%B5%AD%EC%96%B4-%EB%82%A0%EC%A7%9C-%ED%98%95%EC%8B%9D-%EB%B3%80%ED%99%98</guid>
            <pubDate>Fri, 01 Sep 2023 06:55:31 GMT</pubDate>
            <description><![CDATA[<p>C#에서 다양한 날짜 및 시간을 표기할 수 있는데, 다른 국가의 표기법 문자를 읽어와 날짜 형식으로 변환하는 방법에 대해 찾아보았다.</p>
<h3 id="💡-c-datetime-포맷">💡 C# DateTime 포맷</h3>
<p>이번에 사용할 연도, 월, 일에 대해서만 간단하게 포맷을 정리해본다. </p>
<table>
<thead>
<tr>
<th>포맷</th>
<th>설명</th>
<th>예제</th>
</tr>
</thead>
<tbody><tr>
<td>d</td>
<td>일</td>
<td>1, 2, 3,.. 31</td>
</tr>
<tr>
<td>dd</td>
<td>일</td>
<td>01, 02, 03,.. 31</td>
</tr>
<tr>
<td>M</td>
<td>월</td>
<td>1, 2, 3,.. 12</td>
</tr>
<tr>
<td>MM</td>
<td>월</td>
<td>01, 02, 03,.. 12</td>
</tr>
<tr>
<td>MMM</td>
<td>월(약어)</td>
<td>Jan, Feb,...</td>
</tr>
<tr>
<td>MMMM</td>
<td>월</td>
<td>January, February,...</td>
</tr>
<tr>
<td>yy</td>
<td>연도</td>
<td>20, 21, 22,...</td>
</tr>
<tr>
<td>yyyy</td>
<td>연도</td>
<td>2020, 2021, 2022,...</td>
</tr>
</tbody></table>
<br>
<br>

<h3 id="💡-c-다국어-날짜-형식-변환">💡 C# 다국어 날짜 형식 변환</h3>
<p>이 내용에 필요한 <strong>DateTime.ParseExact</strong> 메서드와 <strong>System.Globalization.CultureInfo</strong> 클래스를 사용할 것이다.</p>
<p>각 입력 값에 대한 국가코드와 포맷 형식을 입력하여 매칭시킨 뒤 Date로 받아온 값을 원하는 형식으로 지정하여 사용한다.</p>
<blockquote>
<p>입력 값 예시 : 21 août 2023 (<em>해당 값은 불어에 해당</em>)
출력 값 : 2023-Aug-21</p>
</blockquote>
<ul>
<li>프랑스의 국가코드 : fr-FR</li>
</ul>
<pre><code class="language-cs">(DateTime.ParseExact(&quot;21 août 2023&quot;, &quot;d MMMM yyyy&quot;, CultureInfo.CreateSpecificCulture(&quot;fr-FR&quot;))).ToString(&quot;yyyy-MMM-dd&quot;)</code></pre>
<br>

<blockquote>
<p>입력 값 예시 : 15 noviembre 2022 (<em>해당 값은 스페인어에 해당</em>)
출력 값 : 2022/11/15</p>
</blockquote>
<ul>
<li>스페인의 국가코드 : es-AR</li>
</ul>
<pre><code class="language-cs">(DateTime.ParseExact(&quot;15 noviembre 2022&quot;, &quot;d MMMM yyyy&quot;, CultureInfo.CreateSpecificCulture(&quot;es-AR&quot;))).ToString(&quot;yyyy/MM/dd&quot;)</code></pre>
<br>
]]></description>
        </item>
        <item>
            <title><![CDATA[UiPath Orchestrator Storage Buckets 파일 용량]]></title>
            <link>https://velog.io/@dev_seung/UiPath-Orchestrator-Storage-Buckets-%ED%8C%8C%EC%9D%BC-%EC%9A%A9%EB%9F%89</link>
            <guid>https://velog.io/@dev_seung/UiPath-Orchestrator-Storage-Buckets-%ED%8C%8C%EC%9D%BC-%EC%9A%A9%EB%9F%89</guid>
            <pubDate>Tue, 29 Aug 2023 06:42:24 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@dev_seung/UiPath-Orchestrator-Storage-Buckets-%EA%B2%BD%EB%A1%9C-%EC%88%98%EC%A0%95">이전 글</a>에 이어 스토리지 버킷에 파일 업로드 시 파일 용량에 대해 정리한다.</p>
<h3 id="💡-업로드-파일-크기-제한-늘리기">💡 업로드 파일 크기 제한 늘리기</h3>
<p>UiPath Forum에 찾아봤을 때 별도의 용량 제한이 없다고 답변이 있었지만, 실제로 업로드 시 용량이 크다며 업로드 되지 않은 경우가 발생하였다.
<img src="https://velog.velcdn.com/images/dev_seung/post/c0196688-8a22-4b91-a3b2-5a72d11a4d26/image.png" alt=""></p>
<p>해당 내용에 대해 열심히 해결 방안을 찾았다.</p>
<p>검색도 해보고 서버 컨피그 파일 디비 등.. 여기저기 만져보다가 IIS 관리자에서 수정해줘야한다는 사실을 알게되었다.
<br></p>
<h4 id="📌-파일-크기-제한-늘리는-방법">📌 파일 크기 제한 늘리는 방법</h4>
<ol>
<li>Orcestrator가 설치된 서버에서 서버 관리자 &gt; IIS 관리자 접속</li>
<li>UiPath Orchestrator 홈에서 요청 필터링 접속
<img src="https://velog.velcdn.com/images/dev_seung/post/ab7a9e44-cdcb-4342-bf9e-089d105c40fe/image.png" alt=""></li>
<li>규칙 탭에서 오른쪽 작업 패널에 기능 설정 편집 클릭
<img src="https://velog.velcdn.com/images/dev_seung/post/e1ecab30-9ed0-4b00-8c42-dbacc17b8789/image.png" alt=""></li>
<li>요청 필터링 설정 편집 창에서 허용되는 최대 콘텐츠 길이(바이트) 값 수정
<img src="https://velog.velcdn.com/images/dev_seung/post/b57b8242-21f6-47eb-9c0b-4eb4a380609d/image.png" alt=""></li>
</ol>
<ul>
<li>기본 값은 300MB 이었고, 최대 값은 4GB인 것으로 확인된다.</li>
</ul>
<ol start="5">
<li>서버 재시작<br>

</li>
</ol>
<h4 id="📌-적용-화면">📌 적용 화면</h4>
<p>더미 파일을 최대 늘려논 용량에 맞게 업로드하였을 때 정상적으로 업로드 된 것을 확인할 수 있다. (4GB 기준 업로드 시 5분 정도 소요된다)<img src="https://velog.velcdn.com/images/dev_seung/post/3be20d17-aa53-4a3c-a78a-9c89c43d941e/image.png" alt=""><img src="https://velog.velcdn.com/images/dev_seung/post/49938719-82f5-455e-907c-139276b262fe/image.png" alt=""></p>
<hr>
<br>

<h3 id="💡-더미-파일-생성-방법">💡 더미 파일 생성 방법</h3>
<p>추가로 이러한 테스트를 할 때 용량에 따른 테스트 파일이 필요한데, 파일을 찾는 것도 일이 될 수 있다.
이런 경우 쉽게 더미 파일을 만드는 방법이 있으니 아래 정리해보자</p>
<ol>
<li>PowerShell 열기</li>
<li>명령어 작성
fsutil file createnew {{파일명}} {{용량(바이트)}}<pre><code>fsutil file createnew DummyTestFile 4290000000</code></pre><img src="https://velog.velcdn.com/images/dev_seung/post/ae2583c9-15db-47ba-857b-1f66abbe3e48/image.png" alt=""></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[UiPath Orchestrator Storage Buckets 절대경로 수정]]></title>
            <link>https://velog.io/@dev_seung/UiPath-Orchestrator-Storage-Buckets-%EA%B2%BD%EB%A1%9C-%EC%88%98%EC%A0%95</link>
            <guid>https://velog.io/@dev_seung/UiPath-Orchestrator-Storage-Buckets-%EA%B2%BD%EB%A1%9C-%EC%88%98%EC%A0%95</guid>
            <pubDate>Tue, 29 Aug 2023 05:30:47 GMT</pubDate>
            <description><![CDATA[<h3 id="💡-storage-bucket">💡 Storage Bucket</h3>
<p>스토리지 버킷은 RPA 개발자가 자동화 프로젝트(예: .pdf여러 비즈니스 프로세스에 사용되는 파일 또는 기계 학습 모델에 사용되는 데이터 세트)를 생성하는 데 활용할 수 있는 폴더별 스토리지 솔루션을 제공합니다. </p>
<p>자세한 내용 ⬇️
📍 출처 <a href="https://docs.uipath.com/orchestrator/standalone/2023.4/user-guide/about-storage-buckets">https://docs.uipath.com/orchestrator/standalone/2023.4/user-guide/about-storage-buckets</a></p>
<hr>
<h3 id="💡-서버-절대경로-수정">💡 서버 절대경로 수정</h3>
<p>스토리지 버킷에 파일을 업로드하면 서버 로컬에 저장되는데, 해당 경로를 변경할 수 없을까 싶어 찾아보다가 방법을 알아내 기록으로 남긴다.
<del>파일이 많아지면 용량 이슈가 발생할 수도 있으니...</del>
<br></p>
<p>일단 기존 경로는 Orchestrator 설치 경로
즉, <strong>C:\Program Files (x86)\UiPath\Orchestrator\Storage</strong> 해당 경로에 스토리지 버킷에 업로드한 데이터가 쌓인다.</p>
<h4 id="📌-절대경로-변경-방법">📌 절대경로 변경 방법</h4>
<ol>
<li>오케스트레이터 설치 경로 (C:\Program Files (x86)\UiPath\Orchestrator)</li>
<li>&#39;UiPath.Orchestrator.dll.config&#39; 컨피그 파일 열기 &gt; 관리자 권한 메모장으로 열기</li>
<li>Storage 부분 경로 수정 
 Storage.Location Value 값의 경로를 수정해주면 된다.<pre><code> &lt;!-- Storage 기본 값--&gt;
 &lt;add key=&quot;Storage.Type&quot; value=&quot;FileSystem&quot; /&gt;
 &lt;add key=&quot;Storage.Location&quot; value=&quot;RootPath=.\Storage&quot; /&gt; </code></pre><pre><code> &lt;!-- Storage 수정 값 --&gt;
 &lt;add key=&quot;Storage.Type&quot; value=&quot;FileSystem&quot; /&gt;
 &lt;add key=&quot;Storage.Location&quot; value=&quot;RootPath=E:\Storage&quot; /&gt;</code></pre></li>
<li>서버 재시작<br>

</li>
</ol>
<h4 id="📌-적용-화면">📌 적용 화면</h4>
<p>내가 지정한 경로(E:\Storage)에 업로드된 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/dev_seung/post/58027d9b-9c2e-4fa5-b795-f3255ce947a4/image.png" alt=""><img src="https://velog.velcdn.com/images/dev_seung/post/1818df23-ed95-4080-83c8-216de8971066/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UiPath Document Understanding 교육]]></title>
            <link>https://velog.io/@dev_seung/UiPath-Document-Understanding-%EA%B5%90%EC%9C%A1</link>
            <guid>https://velog.io/@dev_seung/UiPath-Document-Understanding-%EA%B5%90%EC%9C%A1</guid>
            <pubDate>Thu, 10 Aug 2023 08:45:26 GMT</pubDate>
            <description><![CDATA[<h3 id="-du-문서-처리-과정">* DU 문서 처리 과정</h3>
<p>Digitize -&gt; Classify(생략가능) -&gt; Extract
*재학습 가능</p>
<h3 id="-classify-종류">* Classify 종류</h3>
<ol>
<li>Keyword Classifier</li>
</ol>
<ul>
<li>키워드를 사람이 선정</li>
<li>여러개의 키워드를 복합적으로 사용 가능</li>
</ul>
<ol start="2">
<li>Intelligent Keyword Classifier</li>
</ol>
<ul>
<li>키워드를 자동으로 선정</li>
<li>문서 분리 가능 -&gt; 한 파일 내에 여러 문서들이 들어있을 경우 구분해서 잘라주는 기능</li>
</ul>
<ol start="3">
<li>Machine Learning Classifier</li>
</ol>
<ul>
<li>Machine Learning 방식으로 학습</li>
<li>추가 재학습 가능</li>
</ul>
<h3 id="-extract-추출-종류">* Extract (추출) 종류</h3>
<ol>
<li>Rule-based</li>
</ol>
<ul>
<li>RegEx-Based Extractor</li>
<li>Form Extractor (기본적으로 위치 기반/ 보조적으로 주변 앵커 기능 가능)</li>
</ul>
<ol start="2">
<li>AI-based</li>
</ol>
<ul>
<li>Forms AI (적게는 2-3장 많게는 20-30)</li>
<li>Machine Learing Extractor</li>
</ul>
<ol start="3">
<li>Hybrid approach</li>
</ol>
<ul>
<li>A combination of both ― based and AI-based extractors</li>
</ul>
<h4 id="field-rules-validation">Field Rules Validation</h4>
<p>-&gt; 추출된 항목이 업무 요건에 부합하는지 검증하는 과정</p>
<ul>
<li>Mandatory(필수 항목)</li>
<li>Regex</li>
<li>정해진 값보다 크다/ 작다</li>
<li>Net+Tax =Total</li>
<li>미리 정해진 값 중에서만 가능(ex: Currency Code in KRW, USD, EUR)</li>
<li>External data source lookup</li>
</ul>
<h4 id="confidence">Confidence</h4>
<p>-&gt; 보조적으로 일치 여부 확률을 확인할 수 있음</p>
<h4 id="ml-모델-학습에-필요한-최소-문서-수량">ML 모델 학습에 필요한 &quot;최소&quot; 문서 수량</h4>
<ul>
<li>일반 항목 하나 당 50건</li>
<li>테이블 컬럼 하나 당 200건</li>
<li>평가용 문서와 학습용은 별도로 준비 (평가용 문서 20-50건)</li>
</ul>
<p>전처리-Deskew
-&gt; 문서가 틀어진 경우 똑바르게 돌려줌
<a href="https://forum.uipath.com/t/deskew-library-for-document-image-processing/531553">https://forum.uipath.com/t/deskew-library-for-document-image-processing/531553</a></p>
<p>*참고자료
<a href="https://docs.uipath.com/document-understanding/automation-cloud/latest/user-guide/out-of-the-box-pre-trained-ml-packages">https://docs.uipath.com/document-understanding/automation-cloud/latest/user-guide/out-of-the-box-pre-trained-ml-packages</a>
<a href="https://docs.uipath.com/document-understanding/automation-cloud/latest/user-guide/document-manager-use-a-predefined-schema">https://docs.uipath.com/document-understanding/automation-cloud/latest/user-guide/document-manager-use-a-predefined-schema</a>
<a href="https://docs.uipath.com/document-understanding/automation-cloud/latest/user-guide/public-endpoints">https://docs.uipath.com/document-understanding/automation-cloud/latest/user-guide/public-endpoints</a>
<a href="https://ds920.minoh.kr/fsdownload/bugN4woCM/DU_enablement">https://ds920.minoh.kr/fsdownload/bugN4woCM/DU_enablement</a></p>
<hr>
<ul>
<li><p>Taxonomy Manager 사용
아이디명은 필드명을 변경해도 변경 불가</p>
</li>
<li><blockquote>
<p>변경 방법은 json 파일을 수정해야하는데, 권장하지 않음</p>
</blockquote>
</li>
<li><p>Digitize Document Activity
[ Input 값 ]
⊙ Document Path : 분석 파일
⊙ ApplyOcrOnPdf : pdf파일이 이미 텍스트로 추출될 경우 </p>
<ul>
<li>No : 이미 있는 pdf 디지털 텍스트 가져오기 </li>
<li>Yes : 디지털 텍스트가 있어도 OCR로 추출하여 가져오기 </li>
<li>Auto : 디지털 텍스트 있는 건 디지털 텍스트로 하고, 이미지 같은 경우는 OCR로 읽어오기 -&gt; 대부분의 경우 Auto 사용
⊙ DegreeOfParallelism : </li>
<li>-1 : 있는 코어를 하나빼고 다 사용하겠다. (Default)</li>
<li>1 : 있는 코어 하나만을 사용 (페이지 하나씩 읽어오겠다.)</li>
</ul>
</li>
</ul>
<p>[ Output 값 ]
⊙ document text : 문서의 전체 텍스트
⊙ document object model : 문서가 단위별로 분석되는데 단어별로 분석된 데이터</p>
<ul>
<li>Data Extraction Scope Activity
하이브리드로 사용 가능 (여러 Extractor 사용) -&gt; 사용한만큼 Unit이 차감되기 때문에 사용 빈도 수는 낮을 것으로 예상됨
각각 항목들을 어떤 방식으로 추출할 지 설정 가능
ex) Receipt의 name 항목은 Form Extractor로 가져오고, phone 항목은 ML로 가져올 수 있음
단, 같은 항목을 둘 다 체크될 경우 왼쪽에 있는 순서대로 먼저 수행해보고 안되면 우측에 있는 방식으로 진행</li>
</ul>
<p>Configure Extractors &gt; Machine Learning Extractor </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[C# Excel Form 수정]]></title>
            <link>https://velog.io/@dev_seung/C-Code-in-Excel-UiPath-Invoke-Code</link>
            <guid>https://velog.io/@dev_seung/C-Code-in-Excel-UiPath-Invoke-Code</guid>
            <pubDate>Tue, 30 May 2023 04:17:19 GMT</pubDate>
            <description><![CDATA[<p>UiPath를 기반으로 RPA를 개발하다 보면 없는 Activity들이 종종 있다.
물론 UiPath 자체적으로 개발된 다양한 기능을 사용하면 되는 문제도 있지만, 간단하게 코드로 작성하여 사용할 때가 있다.</p>
<p>주로 Excel 관련 업무를 많이 하다보니 Excel 관련 Code를 정리해보자.
<br></p>
<h3 id="기본적인-excel-form-수정">기본적인 Excel Form 수정</h3>
<pre><code class="language-cs">Microsoft.Office.Interop.Excel.Workbook wb = null;
Microsoft.Office.Interop.Excel.Worksheet ws = null;
Microsoft.Office.Interop.Excel.Range rng = null;
wb = in_workbook.CurrentWorkbook;

ws = (Microsoft.Office.Interop.Excel.Worksheet)wb.Worksheets[in_SheetName];

try {
    rng = ws.UsedRange; // 전체 범위 지정

    rng.EntireColumn.Hidden = false; //열 숨기기 해제하기
    rng.EntireRow.Hidden = false; //행 숨기기 해제하기
    ws.AutoFilterMode = false; // 시트 내 필터 제거

    rng.EntireColumn.AutoFit(); // 전체 컬럼 AutoFit
    rng.EntireRow.AutoFit(); // 자동 로우 높이 조절
    rng.HorizontalAlignment = Microsoft.Office.Interop.Excel.XlHAlign.xlHAlignCenter; // 전체 범위 가운데정렬
    rng.Borders.LineStyle = Microsoft.Office.Interop.Excel.XlLineStyle.xlContinuous; // 테두리 설정

    rng = ws.Range[ &quot;A12&quot;, ws.Cells[12, ws.UsedRange.Columns.Count] ]; // 헤더 범위 지정
    rng.Font.Bold = true; // 헤더 굵게
    rng.Borders[Microsoft.Office.Interop.Excel.XlBordersIndex.xlEdgeTop].LineStyle = Microsoft.Office.Interop.Excel.XlLineStyle.xlContinuous;

    ws.Range[&quot;F8&quot;,&quot;H10&quot;].Merge(); // 셀 병합
    ws.Range[&quot;F8&quot;,&quot;H10&quot;].Borders.LineStyle = Microsoft.Office.Interop.Excel.XlLineStyle.xlDouble; // 다른 스타일 테두리
    ws.Range[&quot;F8&quot;,&quot;H10&quot;].Interior.Color = Color.FromArgb(209,178,255); // 배경색 적용
    ws.Range[&quot;F8&quot;,&quot;H10&quot;].Font.Size = 17; // 글씨 크기 조절

    rng = ws.Range[in_str_Range];
    rng.NumberFormat = &quot;#,##0&quot;; // 회계 서식 지정

} catch (Exception e) {
    Console.WriteLine(e.Message);
} finally {
    wb.Save();    
}</code></pre>
<br>
]]></description>
        </item>
        <item>
            <title><![CDATA[Eclipse Spring 프로젝트 생성]]></title>
            <link>https://velog.io/@dev_seung/Eclipse-Spring-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@dev_seung/Eclipse-Spring-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Wed, 19 Apr 2023 05:59:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Eclipse ver. 2022-03 (4.23.0)
Java ver. jdk-11</p>
</blockquote>
<br>

<h3 id="💡-spring-설치">💡 Spring 설치</h3>
<hr>
<p>Help &gt; Eclipse Marketplace<img src="https://velog.velcdn.com/images/dev_seung/post/18ad4830-7882-4114-b002-dc8ba20f3f90/image.png" alt=""></p>
<br>

<h3 id="💡-새-프로젝트-생성">💡 새 프로젝트 생성</h3>
<hr>
<p>New &gt; Other...
Spring Legacy Project &gt; Spring MVC Project
<img src="https://velog.velcdn.com/images/dev_seung/post/f66ef83c-91f2-4f4f-8b46-a556c3c30baa/image.png" alt=""><img src="https://velog.velcdn.com/images/dev_seung/post/ad510d86-3400-4451-9263-e9825757fa85/image.png" alt=""><img src="https://velog.velcdn.com/images/dev_seung/post/4085acbb-72ca-4078-ac18-33a3f20d0ec5/image.png" alt="">package명 작성 : ex) com.test.common</p>
<br>
<hr>

<p><strong>🪄 생성 완료 후 Package Explorer</strong>
<img src="https://velog.velcdn.com/images/dev_seung/post/33e30044-1734-4e51-ae24-c4e62aecf6cf/image.png" alt=""><img src="https://velog.velcdn.com/images/dev_seung/post/f9b48ce4-7e85-4eca-b397-291a8289d6ee/image.png" alt=""> 서버 설치가 완료되었다는 가정하에 프로젝트 생성 후 수행 시 화면
접속 경로 : localhost/common</p>
<br>

<p><strong>🪄 경로 변경 방법</strong>
<img src="https://velog.velcdn.com/images/dev_seung/post/607ed8b9-e758-4d84-af53-f922e8ec180c/image.png" alt=""> 프로젝트 우클릭 &gt; Properties
Web Project Settings 메뉴에서 Context root: / 로 수정</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RPA란?]]></title>
            <link>https://velog.io/@dev_seung/RPA%EB%9E%80</link>
            <guid>https://velog.io/@dev_seung/RPA%EB%9E%80</guid>
            <pubDate>Thu, 23 Mar 2023 04:50:58 GMT</pubDate>
            <description><![CDATA[<h2 id="💡사전적-정의">💡사전적 정의</h2>
<hr>
<p><strong>로보틱 처리 자동화, 로보틱 프로세스 자동화, 로보틱 프로세스 오토메이션(Robotic process automation, RPA)은 소프트웨어 봇 또는 인공지능(AI) 워커(worker)의 개념에 기반을 둔, 최근에 만들어진 비즈니스 프로세스 자동화 기술 형태이다.</strong></p>
<p>전통적인 워크플로 자동화 도구에서 프로그래머는 내부 API 또는 전용 스크립트 언어를 사용하여 백엔드 시스템에 대한 태스크와 인터페이스를 자동화하기 위한 일련의 동작을 생성한다. 이와 대조적으로 RPA 시스템은 사용자가 애플리케이션의 그래픽 사용자 인터페이스(GUI)의 태스크를 수행하는 것을 관찰함으로써 동작 목록을 만든 다음 GUI에 직접 해당 작업을 반복함으로써 자동화를 수행한다. 이로써 제품 자동화 이용에 대한 장벽을 낮출 수 있다.</p>
<p>RPA 도구들은 그래픽 사용자 인터페이스 테스트 도구와 기술적으로 상당한 유사점이 있다. 해당 도구들 또한 GUI를 사용하여 상호작용을 자동화하고 사용자가 수행한 일련의 동작을 반복함으로써 이를 수행한다.</p>
<p><em>📍 출처 : 위키백과</em></p>
<blockquote>
<p>한줄 요약 : 정형화된 반복적인 업무 대신 수행해주는 소프트웨어 로봇</p>
</blockquote>
<br>

<h2 id="💡도입-배경">💡도입 배경</h2>
<hr>
<p>우리 사회는 고령화 및 생산 가능 인구 감소 그리고 주 52시간 노동을 기본으로 일과 삶의 균형을 맞추려는 추세로 이런 환경에서 기업들은 근로자의 노동시간 부족과 작업의 낮은 생산성이 중요한 문제로 대두되면서 이에 대응하여 노동력 부족을 극복하고 작업생산성 향상을 위한 4차 산업혁명의 다양한 기술들을 기업 경영 전반에 활용하고 있다.</p>
<p> 그러한 과정 중 인간의 노동을 디지털 노동(Digital Labor)으로 대체하여 경쟁력을 높이려는 생각을 하게 되었으며 RPA(Robotic Process Automation)의 도입으로 기업의 문제해결과 경쟁력을 강화할 수 있게 되었다.</p>
<p>그동안 인간에 의해 진행되던 규칙적이고 반복적인 단순 사무 업무뿐만 아니라 전문지식에 기반한 고도의 의사결정까지 지원을 목표로 하고 있으며 제조업의 로봇이 그랬던 것처럼 RPA는 사무 현장의 노동 인력 구조에 중대한 변화를 가져올 것으로 예상하고 있다.
<img src="https://velog.velcdn.com/images/dev_seung/post/eb42a0fa-0894-42a2-80b3-916bcc02c136/image.png" alt="">기업의 사무자동화는 경영 지원 업무에 최적화된 ‘ERP(Enterprise Resource Planning)’를 시작으로 기업의 핵심 업무를 제외한 과정을 외부 업체에 맡기는 아웃소싱 방식의 ‘BPO(Business Process Outsourcing)’를 거쳐왔다.</p>
<p>RPA은 앞선 두 방식보다 ‘신속성’과 ‘확장성’이라는 장점을 가진다. 기업에 적용하기 위해 많은 시간과 비용이 요구 됐던 앞선 두 방식과는 달리, RPA는 신규 인터페이스 개발 없이 기존 시스템을 기반으로 소프트웨어만 구현하는 것이기에 빠른 구축과 실현이 가능했기 때문이다. 또한 RPA는 ERP와 BPO 적용 이후에도 사람의 처리가 필요했던 단순 업무들을 축소 하면서, 구성원들이 보다 고부가가치 활동에 집중할 수 있다는 장점도 있다.</p>
<br>

<h2 id="💡도입-효과">💡도입 효과</h2>
<hr>
<p><img src="https://velog.velcdn.com/images/dev_seung/post/9bf03ee4-9f72-41d2-8cf7-e9f73253bf7a/image.png" alt="">주요 선진국에서 처음 RPA가 주목받은 배경에는 인건비 절감 효과가 있다. NH투자증권에 따르면 RPA 도입은 인력의 직접 고용에 비해 80~90% 정도 비용을 절감할 수 있다. 구글·테슬라·소니·보쉬·EMC 등 다수의 글로벌 기업들이 RPA를 도입, 운영하고 있다. 일본에서는 야근 규제 등을 담은 ‘일하는 문화 개혁 관련법’이 지난해 4월 1일부터 적용되면서 업무 시간이 단축되고 있어 인력을 대체하는 기술로 RPA에 주목했다. 도요타자동차·SMBC·덴츠 등이 도입을 시작해 점차 확대되는 추세다.</p>
<p>국내에서는 주52시간 근무제 도입 이후 RPA가 확산되고 있다. 직장인들의 하루 업무 시간이 줄어들면서 불 꺼진 사무실을 사무 로봇이 채우고 있다. 특히 주52시간 근무를 1년 유예 받은 금융권에서 적극적으로 도입하기 시작했다. 금융에 이어 제조업과 서비스업 분야로도 확산되는 추세다. 여기에 최근 코로나19로 재택근무가 활성화되면서 업무 효율성을 높이는 기술로 주목받고 있다.</p>
<p>LG전자는 디지털 전환을 가속화하기 위해 올해 연말까지 RPA 적용 업무를 2배 가까이 늘리기로 했다. 스마트 워크 차원에서 2018년부터 회계·인사·영업·마케팅·구매 등 사무직 분야 약 500개 업무에 RPA 기술을 도입한 데 이어 올해부터 지능형 RPA를 속속 도입하고 있다. LG전자 내에서 현재 RPA가 처리하는 업무량은 사람 기준으로 환산하면 월 1만2000시간에 해당한다.
<img src="https://velog.velcdn.com/images/dev_seung/post/d545e04f-1440-4908-bdf4-d1992bc9b8f6/image.png" alt=""></p>
<br>

]]></description>
        </item>
    </channel>
</rss>