thruthesky / centerx

Docker Enginx + MariaDB + PHP
2 stars 0 forks source link

주요 개발 요소

CenterX 는 Theme 별로 개별 사이트를 만들 수 있는데, 아래의 요소들을 사용한다.

만약, Vue.js, Bootstrap, Font Awesome 등을 사용하지 않고, 다른 자바스크립트, CSS, 아이콘 등을 사용하고 싶다면 그렇게 하면 된다. 다만, CenterX 에서 제공하는 기본 위젯들이 Vue.js 와 Bootstrap, Font Awesome 을 기본으로 만들어져 있다. 그래서 이미 만들어진 위젯 기능을 사용하려 한다면, 위의 요소들을 사용해야 한다.

세부 개발 요소

문서 안내

목표

수정 사항 안내

해야 할 일

간단하게 아래와 같이 코딩을 해서, 어디서든 실행을 한번 하면 된다.

user()->by('thruthesky@gmail.com')->changePassword('12345a');

Center X 의 데이터 모델

데이터 저장

기본적으로 각 테이블에 저장하며, 테이블에 없는 값은 meta 로 저장한다.

이 때, 쓸데 없는 값이 저장되지 않도록 defines 에 정의를 해서 처리를 하지만

악용을 하기 위해 큰 데이터를 저장하려는 경우가 있다.

이 때에는 입력값에 허용된 값이 아닌 다른 값이 들어오면, 저장을 하지 않던지 에러를 내야한다.

현재는 이 기능이 없으며, 훅을 통해서 처리를 해야 한다.

객체를 리턴하지 않는 bool, string, int 등의 함수는 리턴값은 객체가 기본이다.

다만, API 로 전달하는 경우, 에러가 있으면 문자열, 에러가 없으면 배열로 리턴한다.

문서화

cd etc/phpdoc
./phpDocumentor

Configuration

Theme Configuration

Functions

Developer Guideline

Entity and Taxonomy

Creating or Updating an Entity

$obj = token()->findOne([TOKEN => '...token...', TOPIC => '..topic...']);
if ( $obj->exists ) {
    $obj->update( [ USER_IDX => login()->idx ] );
} else {
    $obj->create([
        USER_IDX => login()->idx,
        TOKEN => '...token...',
        TOPIC => '..topic...',
    ]);
}

Taxonomy helper classes

Each taxonomy may have its own customised methods. Here are some recommendations on creating helper methods on each taxonomies.

Posts Taxonomy

새로운 사이트 만들기

Nginx 설정 예제)

# dating project
server {
    server_name  dating.com;
    listen       80;
    root /docker/home/centerx;
    index index.php;
    location / {
        try_files $uri $uri/ /index.php?$args;
    }
    location ~ \.php$ {
        fastcgi_pass   php:9000;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
}

User

User login and registration tag

Friendly URL

회원 가입

어서오세요, <?=login()->nicknameOrName?>
<hr>
<form>
    <input type="hidden" name="p" value="user.register.submit">
    <input type="email" name="email" value=""">
    <input type="text" name="password" value=""">
    <button type="submit">회원가입</button>
</form>
$user = user()->register(in());
if ( $user->hasError ) jsAlert($user->getError());
else {
    setLoginCookies($user->profile());
    jsGo('/');
}

회원 가입을 Javascript 로 XHR 호출을 통해서 하는 경우,

<script>
  mixins.push({
    data: {
      form: {
        email: '',
        password: '',
      }
    },
    methods: {
      onSubmitRegisterForm: function() {
        request('user.register', this.form, function(user) {
          Cookies.set('sessionId', user.sessionId, {
            expires: 365,
            path: '/',
            domain: '<?=COOKIE_DOMAIN?>'
          });
        }, alert);

      }
    }
  })
</script>

Cookies

Cookies between PHP and Javascript

setAppCookie('sessionId', '3330-9622d005fbba90d96ea1a967e142a5ce');

Sharing Code between PHP and Javascript

Same functions that exist both on PHP and Javascript.

Similiar functions

Widget System

/**
 * @name Abc
 * @desc Description of Abc
 * @type ...
 */

Widget samples

재 사용가능한 위젯 샘플, Making Re usable widgets

위젯 상속

예) post-edit-default.php 에서 입력 양식이 대충 아래와 같다. 이 때, 상단 제목이나 언어화를 아래의 예제와 같이 변경 할 수 있다.

<form action="/" method="POST">
    <input type="hidden" name="p" value="forum.post.edit.submit">
    ...

    <?=hook()->run('post-edit-title') ?? "<h6>Category: {$category->id}</h6>" ?>
    <input placeholder="<?= ln('input_title') ?>" name="<?= TITLE ?>" value="<?= $post->v(TITLE) ?>">

예)

translate('input_title', [
    'en' => 'Input message title.',
    'ko' => '쪽지 제목을 입력하세요.',
]);
translate('input_content', [
    'en' => 'Input message content.',
    'ko' => '쪽지 내용을 입력하세요.',
]);

hook()->add('post-edit-title', function() {
    return '쪽지 전송';
});
include widget('post-edit/post-edit-default');

Post Crud

글 작성, Post Edit

글 작성 위젯 옵션

글 작성 위젯 옵션 활용 - 이벤트 게시판 등

Post Edit Widget 중에서 코드 별 사진 업로드(post-edit-upload-by-code.php)가 있다.

이 위젯은, 글의 제목과 내용을 업로드 할 수 있으며, 첨부 파일/사진을 코드 별로 업로드 할 수 있다. 즉, 임의의 사진을 무한정 업로드 할 수 없고, 정해진 코드 몇 개에만 업로드 가능하다.

글 읽기는 적절한 위젯을 만들어 사용하면 된다.

예를 들어, 이벤트 게시판을 작성한다고 가정 할 때,

이벤트 배너를 목록에 보여주고, 이벤트 내용을 사진으로 보여주고자 할 때,

사진 2개만 입력 받을 수 있다.

이 때, 아래와 같이 입력을 하면 된다.

[upload-by-code]
banner[label]=배너 사진
banner[tip]=목록에 나타나는 이벤트 배너(광고) 사진을 업로드 해 주세요.
content[label]=내용 사진
content[tip]=배너 사진을 클릭 했을 때 나타나는 내용 사진을 업로드 해 주세요.

위와 같이 하면, 목록에서 배너 사진을 보여주고, 내용에 내용 사진을 보여주면 된다.

첨부 파일의 코드에 'banner' 또는 'content' 가 들어간다.

만약, 이벤트가 종료되었다면, 배너 사진에 문구를 종료됨으로 수정하고, 내용에도 종료되었다는 표시를 하면 된다.

Firebase

define('FIREBASE_SDK_ADMIN_KEY', <<<EOJ
{
    apiKey: "AIzaSyDWiVaWIIrAsEP-eHq6bFBY09HLyHHQW2U",
    authDomain: "sonub-version-2020.firebaseapp.com",
    databaseURL: "https://sonub-version-2020.firebaseio.com",
    projectId: "sonub-version-2020",
    storageBucket: "sonub-version-2020.appspot.com",
    messagingSenderId: "446424199137",
    appId: "1:446424199137:web:f421c562ba0a35ac89aca0",
    measurementId: "G-F86L9641ZQ"
}
EOJ);

Firebase Javascript

<script>
    later(function() {
        const db = firebase.firestore();
        console.log(db);
        db.collection('notifications').doc('settings').set({time: (new Date).getTime()});
    })
</script>
<?php
    includeFirebase();
?>

Meta

You can search meta table directly by using meta().

When you are saving extra data into user taxonomy, the extra data, which does not exists as users table record, goes into meta table. You can search directly by code as the name of the data. Or with the combination of taxonomy.

If it's user extra data, then the taxonomy is users. If it's post(or comment) extra data, then the taxonomy is posts.

$meta = meta()->get('code', 'topic_qna');
$metas = meta()->search("taxonomy='users' AND code='topic_qna' AND data='Y'", select: 'entity', limit: 10000);

The above code is identical as below

$meta = entity(METAS)->get('code', 'topic_qna');
$metas = entity(METAS)->search("taxonomy='users' AND code='topic_qna' AND data='Y'", select: 'entity', limit: 10000);

파일 업로드

Firebase

Vote

Translation

자바스크립트에서 언어화

In-page Translation

<?=ln('name')?>

자바스크립트 - common.js

Change language

<form class="m-0 p-0" action="/">
    <input type="hidden" name="p" value="user.language.submit">
    <label class="m-0 p-2">
        <select name="language" onchange="submit()">
            <option value="">언어선택</option>
            <option value="ko">한국어</option>
            <option value="en">English</option>
        </select>
    </label>
</form>

Settings

테마 Theme

테마 폴더 구조

테마 페이지 로딩. Loading theme page.

app.js

인라인 스타일 태그와 자바스크립트 태그를 하단으로 이동

외부 Javascript 를 포함하기

<?php js(HOME_URL . 'etc/js/common.js', 7)?>
<?php js(HOME_URL . 'etc/js/vue.2.6.12.min.js', 9)?>
<?php js(HOME_URL . 'themes/sonub/js/bootstrap-vue-2.21.2.min.js', 10)?>
<?php js(HOME_URL . 'etc/js/common.js', 10)?>
<?php js(HOME_URL . 'etc/js/common.js', 10)?>
<?php js(HOME_URL . 'etc/js/common.js', 10)?>
<?php js(HOME_URL . 'etc/js/common.js', 10)?>
<?php js(HOME_URL . 'etc/js/app.js', 0)?>

HTTP 입력 값 조정

웹 브라우저가 서버로 /?a.b.c 와 같이 접속하면 테마에서 특정 페이지를 로드 할 때, theme()->pageName()theme()->file() 의 조합으로 어떤 페이지를 로드할 지 결정을 한다.

하지만, 웹 브라우저의 접속 경로가 /?a.b.submit&a=apple&b=banana 와 같이 .submit 으로 들어오면 테마 자체가 로드되지 않아야 한다. 즉, 그 경로의 판단을 테마가 로드되기 전에 해야 한다.

그래서, boot.php 에서 HTTP 입력 값 조정을 한다.

.submit 을 p 변수로 변환

주의, PHP 공식 문서: Dot 이 Underscore 로 변경됨에 따라 /?a.b.submit 이 PHP 로 전달될 때, HTTP 변수로 인식되어 PHP 의 $_REQUEST 에 $_REQUEST['a_b_submit'] 로 저장되고 값은 없으므로 빈 값이 저장된다.

즉, /?a.b.submit&a=apple&b=banana 와 같이 접속하면 아래와 같이 $_REQUEST 에 저장되는 것이다.

Array
(
    [a_b_submit] => 
    [a] => apple
    [b] => banana
)

이 점을 활용하여, $_REQUEST 의 key 중에 _submit 으로 끝나는 것이 있으면, 테마를 표시하지 않고, 오직, PHP 를 통해 값을 저장하는 것으로 인식하여, index.php 에서 테마를 로드하지 않고, PHP 스크립트에서만 처리를 한다.

이러한 작업은 adjust_http_input() 에서 하며 (이 뿐만 아니라 여러가지 다른 작업이 있을 수 있다.), 그 결과로

/?a.b.submit&a=apple&b=banana 와 같이 접속을 하면 아래와 같이 .submit 의 값이 p 변수에 저장된다.

Array
(
    [a] => apple
    [b] => banana
    [p] => a.b.submit
)

Vue.js 2 사용

Bootstrap 4 사용

Bootstrap Vue 2.x 사용

예제) 아래와 같이 polyfill.min.js 를 포함해야 IE10+ 이상이 지원된다.

<!-- Load polyfills to support older browsers before loading Vue and Bootstrap Vue -->
<script src="https://github.com/thruthesky/centerx/raw/main//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<?php js(HOME_URL . 'etc/js/vue.2.6.12.min.js', 2)?>
<?php js(HOME_URL . 'etc/js/bootstrap-vue-2.21.2.min.js', 1)?>

아이콘, SVG

Admin page design

<h1>Admin Page</h1>
<a href="https://github.com/thruthesky/centerx/blob/main/?p=admin.index&w=user/admin-user-list">User</a>
<a href="https://github.com/thruthesky/centerx/blob/main/?p=admin.index&w=category/admin-category-list">Category</a>
<a href="https://github.com/thruthesky/centerx/blob/main/?p=admin.index&w=category/admin-post-list">Posts</a>
<a href="https://github.com/thruthesky/centerx/blob/main/?p=admin.user.list">Mall</a>
<?php
include widget(in('w') ?? 'user/admin-user-list');
?>

- When you submit the form, the may code like below.
  - Below are the category create sample.
  - When the form is submitted, it reloads(redirect to the current page) and create the category.

```html
<?php
if ( modeCreate() ) {
    $re = category()->create([ID=>in('id')]);
}
?>
<form>
  <input type="hidden" name="p" value="admin.index">
  <input type="hidden" name="w" value="category/admin-category-list">
  <input type="hidden" name="mode" value="create">
  <input type="text" name='id' placeholder="카테고리 아이디 입력">
  <button type="submit">Create</button>
</form>

FORM

예제) 먼저 훅을 추가하고

<?php
hook()->add(HOOK_POST_EDIT_RETURN_URL, function() {
    return 'list';
});
?>

예제) 아래와 같이 쓰면 된다.

<form action="/" method="POST" <?=hook()->run(HOOK_POST_EDIT_FORM_ATTR)?>>
    <?php
    echo hiddens(
        p: 'forum.post.edit.submit',
        return_url: hook()->run(HOOK_POST_EDIT_RETURN_URL) ?? 'view',
        kvs: $hidden_data,
    );
    ?>

글 쓰기

Vue.js 3

<section id="app">
  <input type="number" @keyup="onPriceChange" v-model="post.price">
</section>
<script src="https://github.com/thruthesky/centerx/raw/main/<?=ROOT_URL?>/etc/js/vue.3.0.7.global.prod.min.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                post: {
                    price: 1
                }
            }
        },
        created() {
            console.log('created');
        }
    }).mount("#app");
</script>

파이어베이스

Firestore 퍼미션

    /// Notifications
    match /notifications/{docId} {
      allow read: if true;
      allow write: if true;
    }

Markdown

<?php
$md = file_get_contents(theme()->folder . 'README.md');
include_once ROOT_DIR . 'etc/markdown/markdown.php';
echo Markdown::render ($md);
?>

Unit Testing

사진업로드, 파일업로드

Vue.js 2 로 만든 가장 좋은 코드 (중요)

글 작성시 사진 업로드 - post-edit-form-file 믹스인

코멘트 컴포넌트 - comment-form 컴포넌트

사진을 업로드하는 - upload-image 컴포넌트

<upload-image taxonomy="<?=cafe()->taxonomy?>" entity="<?=cafe()->idx?>" code="logo"></upload-image>
<style>
  .uploaded-image img {
    width: 100%;
  }
  .upload-button {
    margin-top: .5em;
  }
</style>
<?php js('/etc/js/vue-js-components/upload-image.js')?>

글에 코드 별로 사진을 업로드 - upload-by-code 컴포넌트

<upload-by-code post-idx="<?=$post->idx?>" code="primaryPhoto" label="대표 사진" tip="상품 페이지 맨 위에 나오는 사진"></upload-by-code>

Vue.js 를 사용한 예제

<?php
$post = post(in(IDX, 0));
if ( in(CATEGORY_ID) ) {
    $category = category( in(CATEGORY_ID) );
} else if (in(IDX)) {
    $category = category( $post->v(CATEGORY_IDX) );
} else {
    jsBack('잘못된 접속입니다.');
}
?>
<div id="post-edit-default" class="p-5">
    <form action="/" method="POST">
        <input type="hidden" name="p" value="forum.post.edit.submit">
        <input type="hidden" name="return_url" value="view">
        <input type="hidden" name="MAX_FILE_SIZE" value="16000000" />
        <input type="hidden" name="files" v-model="files">
        <input type="hidden" name="<?=CATEGORY_ID?>" value="<?=$category->v(ID)?>">
        <input type="hidden" name="<?=IDX?>" value="<?=$post->idx?>">
        <div>
            title:
            <input type="text" name="<?=TITLE?>" value="<?=$post->v(TITLE)?>">
        </div>
        <div>
            content:
            <input type="text" name="<?=CONTENT?>" value="<?=$post->v(CONTENT)?>">
        </div>
        <div>
            <input name="<?=USERFILE?>" type="file" @change="onFileChange($event)" />
        </div>
        <div class="container photos">
            <div class="row">
                <div class="col-3 col-sm-2 photo" v-for="file in uploadedFiles" :key="file['idx']">
                    <div clas="position-relative">
                        <img class="w-100" :src="https://github.com/thruthesky/centerx/raw/main/file['url']">
                        <div class="position-absolute top left font-weight-bold" @click="onFileDelete(file['idx'])">[X]</div>
                    </div>
                </div>
            </div>
        </div>
        <div>
            <button type="submit">Submit</button>
        </div>
    </form>
</div>

<script>
  alert('fix to vu2;');
    const postEditDefault = Vue.createApp({
        data() {
            return {
                percent: 0,
                files: '<?=$post->v('files')?>',
                uploadedFiles: <?=json_encode($post->files(), true)?>,
            }
        },
        created () {
            console.log('created() for post-edit-default');
        },
        methods: {
            onFileChange(event) {
                if (event.target.files.length === 0) {
                    console.log("User cancelled upload");
                    return;
                }
                const file = event.target.files[0];
                fileUpload(
                    file,
                    {
                        sessionId: '<?=login()->sessionId?>',
                    },
                    function (res) {
                        console.log("success: res.path: ", res, res.path);
                        postEditDefault.files = addByComma(postEditDefault.files, res.idx);
                        postEditDefault.uploadedFiles.push(res);
                    },
                    alert,
                    function (p) {
                        console.log("pregoress: ", p);
                        this.percent = p;
                    }
                );
            },
            onFileDelete(idx) {
                const re = confirm('Are you sure you want to delete file no. ' + idx + '?');
                if ( re === false ) return;
                axios.post('/index.php', {
                    sessionId: '<?=login()->sessionId?>',
                    route: 'file.delete',
                    idx: idx,
                })
                    .then(function (res) {
                        checkCallback(res, function(res) {
                            console.log('delete success: ', res);
                            postEditDefault.uploadedFiles = postEditDefault.uploadedFiles.filter(function(v, i, ar) {
                                return v.idx !== res.idx;
                            });
                            postEditDefault.files = deleteByComma(postEditDefault.files, res.idx);
                        }, alert);
                    })
                    .catch(alert);
            }
        }
    }).mount("#post-edit-default");
</script>

Vue.js 2 로 하나의 글에 코드 별 여러 사진(파일) 업로드 & 컴포넌트로 작성

<?php
$post = post(in(IDX, 0));

if ( in(CATEGORY_ID) ) {
    $category = category( in(CATEGORY_ID) );
} else if (in(IDX)) {
    $category = category( $post->v(CATEGORY_IDX) );
} else {
    jsBack('잘못된 접속입니다.');
}
?>
<style>
    .size-80 { width: 80px; height: 80px; }
</style>
<div id="itsuda-event-edit" class="p-5">
    <form action="/" method="POST">
        <input type="hidden" name="p" value="forum.post.edit.submit">
        <input type="hidden" name="return_url" value="view">
        <input type="hidden" name="MAX_FILE_SIZE" value="16000000" />
        <input type="hidden" name="<?=CATEGORY_ID?>" value="<?=$category->v(ID)?>">
        <input type="hidden" name="<?=IDX?>" value="<?=$post->idx?>">
        <input type="hidden" name="files" v-model="files">

        <div class="form-group">
            <small id="" class="form-text text-muted">이벤트 제목을 적어주세요.</small>
            <label for="">제목</label>
            <div>
                <input class="w-100" type="text" name="title" value="<?=$post->title?>">
            </div>
        </div>

        <div class="form-group">
            <small id="" class="form-text text-muted">이벤트 내용을 적어주세요.</small>
            <small class="form-text text-muted">이벤트 날짜, 담청자 목록 등을 적을 수 있습니다.</small>
            <label for="">내용</label>
            <div>
                <textarea class="w-100" rows="10" type="text" name="content"><?=$post->content?></textarea>
            </div>
        </div>
        <hr>
        <photo-upload post-idx="<?=$post->idx?>" code="banner" label="배너 사진" tip="배너 사진을 등록해 주세요. 너비 4, 높이 1 비율로 업로드해 주세요."></photo-upload>
        <photo-upload post-idx="<?=$post->idx?>" code="content" label="내용 사진" tip="내용 사진을 업로드 해 주세요."></photo-upload>

        <div>
            <button type="submit">Submit</button>
        </div>
    </form>
</div>

<script>
    Vue.component('photo-upload', {
        props: ['postIdx', 'code', 'label', 'tip'],
        data: function() {
            return {
                percent: 0,
                file: {},
            }
        },
        created: function() {
            console.log('created for', this.postIdx, this.code);
            if ( this.postIdx ) {
                const self = this;
                request('file.byPostCode', {idx: this.postIdx, code: this.code}, function(res) {
                    console.log('byPostCode: ', res);
                    self.file = Object.assign({}, self.file, res);
                }, alert);
            }
        },
        template: '' +
            '<section class="form-group">' +
            '   <small class="form-text text-muted">{{tip}}</small>' +
            '   <label>{{label}}</label>' +
            '   <img class="mb-2 w-100" :src="https://github.com/thruthesky/centerx/raw/main/file.url">' +
            '   <div>' +
            '       <input type="file" @change="onFileChange($event, \'banner\')">' +
            '   </div>' +
            '</section>',

        methods: {
            onFileChange: function (event) {
                if (event.target.files.length === 0) {
                    console.log("User cancelled upload");
                    return;
                }
                const file = event.target.files[0];
                const self = this;

                // 이전에 업로드된 사진이 있는가?
                if ( this.file.idx ) {
                    // 그렇다면, 이전 업로드된 파일이 쓰레기로 남지 않도록 삭제한다.
                    console.log('going to delete');
                    request('file.delete', {idx: self.file.idx}, function(res) {
                        console.log('deleted: res: ', res);
                        self.$parent.$data.files = deleteByComma(self.$parent.$data.files, res.idx);
                    }, alert);
                }

                // 새로운 사진을 업로드한다.
                fileUpload(
                    file,
                    {
                        code: self.code
                    },
                    function (res) {
                        console.log("success: res.path: ", res, res.path);
                        self.$parent.$data.files = addByComma(self.$parent.$data.files, res.idx);
                        self.file = res;
                    },
                    alert,
                    function (p) {
                        console.log("("+self.code+")pregoress: ", p);
                        this.percent = p;
                    }
                );
            },
        },
    });

    mixins.push({
        data: {
            files: '<?=$post->v('files')?>',
        },
    });
</script>

Vue.js 3 로 특정 코드로 이미지를 업로드하고 관리하는 방법

<?php
$file = files()->getByCode(in('code'));
?>
<section id="admin-upload-image">
  <form>
    <div class="position-relative overflow-hidden">
      <button class="btn btn-primary" type="submit">사진 업로드</button>
      <input class="position-absolute left top fs-lg opacity-0" type="file" @change="onFileChange($event)">
    </div>
  </form>
  <div v-if="percent">업로드 퍼센티지: {{ percent }} %</div>
  <hr>
  <div class="" v-if="src">
    <img class="w-100" :src="https://github.com/thruthesky/centerx/raw/main/src">
  </div>
</section>

<script>
  alert('fix to vu2;');
  const adminUploadImage = Vue.createApp({
    data() {
      return {
        percent: 0,
        src: "<?=$file->url?>"
      }
    },
    mounted() { console.log("admin-upload-image 마운트 완료!"); },
    methods: {
      onFileChange(event) {
        if (event.target.files.length === 0) {
          console.log("User cancelled upload");
          return;
        }
        const file = event.target.files[0];
        fileUpload( // 파일 업로드 함수로 파일 업로드
                file,
                {
                  sessionId: '<?=login()->sessionId?>',
                  code: '<?=in('code')?>',
                  deletePreviousUpload: 'Y'
                },
                function (res) {
                  console.log("파일 업로드 성공: res.path: ", res, res.path);
                  adminUploadImage.src = res.url;
                  adminUploadImage.percent = 0;
                  axios({ // 파일 업로드 후, file.idx 를 관리자 설정에 추가.
                    method: 'post',
                    url: '/index.php',
                    data: {
                      route: 'app.setConfig',
                      code: '<?=in('code')?>',
                      data: res.idx
                    }
                  })
                          .then(function(res) { console.log('app.setConfig success:', res); })
                          .catch(function(e) { conslole.log('app.setConfig error: ', e); })
                },
                alert, // 에러가 있으면 화면에 출력.
                function (p) { // 업로드 프로그레스바 표시 함수.
                  console.log("업로드 퍼센티지: ", p);
                  adminUploadImage.percent = p;
                }
        );
      },
    }
  }).mount("#admin-upload-image");
</script>

바닐라 자바스크립트를 사용하여, 코드 별 파일 업로드 예제

<script>
    function onFileChange(event, id) {
        const file = event.target.files[0];
        fileUpload(
            file,
            {
                sessionId: '<?=login()->sessionId?>',
            },
            function (res) {
                console.log("success: res.path: ", res, res.path);
                const $input = document.getElementById(id);
                $input.value = res.idx;
                const $img = document.getElementById(id + 'Src');
                $img.src = res.url;
            },
            alert,
            function (p) {
                console.log("pregoress: ", p);
            }
        );
    }

    function onClickFileDelete(idx, id) {
        const re = confirm('Are you sure you want to delete file no. ' + idx + '?');
        if ( re === false ) return;
        axios.post('/index.php', {
            sessionId: '<?=login()->sessionId?>',
            route: 'file.delete',
            idx: idx,
        })
            .then(function (res) {
                const $input = document.getElementById(id);
                $input.value = '';
                const $img = document.getElementById(id + 'Src');
                $img.src = '';
            })
            .catch(alert);
    }
</script>

Vue.js 3 로 코드 별 파일을 업로드하고, 기존에 업로드된 파일을 삭제하는 코드

Vue.js 2 로 서버와 통신하는 예제

function request(route, params, success, error) {
  if ( ! params ) params = {};
  // If user has logged in, attach session id.
  if ( Cookies.get('sessionId') ) params['sessionId'] = Cookies.get('sessionId');
  params['route'] = route;
  axios.post('/index.php', params).then(function(res) {
    if (typeof res.data.response === 'string' ) error(res.data.response);
    else success(res.data.response);
  }).catch(error);
}
request('app.version', {}, console.log, console.error);
request('user.profile', {}, console.log, console.error);

Vue.js 2 로 게시글 읽기 페이지에서 코멘트와 사진을 생성하고 수정, 삭제하는 예제

카페

PWA

Service worker

The goal of service worker registration is NOT to do the service worker cache, but to do app banner installation.

<script>
    // Check that service workers are supported
    if ('serviceWorker' in navigator) {
        // Use the window load event to keep the page load performant
        window.addEventListener('load', () => {
            navigator.serviceWorker.register('/themes/sonub/js/service-worker.js.php', {
                scope: '/'
            });
        });
    }
</script>

start_url

Geo IP, GeoLite2

플러터로 새로운 앱을 개발 할 때 해야하는 것

퍼미션 검사

언어화, Translation, 번역

테마별 언어화

global $translations;
$translations = array_merge($translations, [
    'cafe_admin' => [
        'en' => 'Cafe Admin',
        'ko' => '카페 관리자'
    ]
]);

Vue.js 2 Mixins

글 및 posts taxonomy 관련 레코드 작성(수정)시 첨부 파일

Vue.js 2 component

upload-by-code


<input type="hidden" name="files" v-model="files">

<upload-by-code post-idx="<?=$post->idx?>" code="primaryPhoto" label="대표 사진" tip="상품 페이지 맨 위에 나오는 사진"></upload-by-code>
<upload-by-code post-idx="<?=$post->idx?>" code="widgetPhoto" label="위젯 사진" tip="각종 위젯에 표시"></upload-by-code>
<upload-by-code post-idx="<?=$post->idx?>" code="detailPhoto" label="설명 사진" tip="상품을 정보/설명/컨텐츠를 보여주는 사진"></upload-by-code>

<script>
    mixins.push({
        data: {
            files: '<?=$post->v('files')?>',
        },
    });
</script>

Default Javascript

글을 관련 로직, 최근 글 가져오는 로직 등

위젯

글 목록 위젯

post-list-top.php

글 목록 상단 위젯

글 쓰기 위젯

글 쓰기 위젯에 들어가는 요소

<div v-if="!loading">
  <button class="btn btn-warning mr-3" type="button" onclick="history.go(-1)"><?=ln('cancel')?></button>
  <button class="btn btn-success" type="submit"><?=ln('submit')?></button>
</div>
<div class="d-none" :class="{'d-block': loading }">
  전송중입니다.
</div>
<script>
    mixins.push({
        data: {
            loading: false,
            files: '<?= $post->v('files') ?>',
            percent: 0,
            uploadedFiles: <?= json_encode($post->files(true), true) ?>,
        }
    });
</script>

코멘트 위젯

<comment-form root-idx="<?= $post->idx ?>"
              parent-idx='<?= $comment->idx ?>'
              text-photo="<?=ln('photo')?>"
              text-submit="<?=ln('submit')?>"
              text-cancel="<?=ln('cancel')?>"
              v-if="displayCommentForm[<?= $comment->idx ?>] === 'reply'"
></comment-form>

Ajax 로 글 전송 위젯, post-edit-ajax.php

post-edit-upload-by-code

[upload-by-code]
banner[label]=배너
banner[tip]=배너입니다.
primary[label]=대표사진
primary[tip]=대표사진입니다.
content[label]=내용사진
content[tip]=내용사진입니다.

PhpStorm Settings

"ext-mysqli": "*"

CSS, 공용 CSS

x.scss, x.css

progress bar

Error code

문제 해결

웹 브라우저로 접속이 매우 느린 경우

live reload 를 위한 socket.io.js 를 로드하는데 이것이 pending 상태로되어져 있는지 확인한다.

socket.io.js 를 로드하지 않던지, 또는 pending 아닌 failed 로 되어야 한다. 그렇지 않고 pending 상태이면, 웹 브라우저에서 매우 느리게 접속 될 수 있다.

GET socket.io.js net::ERR_EMPTY_RESPONSE

Live reload 코드가 실행되는데, socket.io.js 를 로드하지 못해서 발생한 에러.

config.php 에서 LIVE_RELOAD_HOST 에 빈 문자열 값을 주어, live reload 를 turn off 할 수 있다.

error_entity_not_found on CountryTaxonomy

Be sure you have the countries table records into the wc_countries table.

Firebase - Invalid service account

Next Kreait\Firebase\Exception\InvalidArgumentException: Invalid service account: /docker/home/centerx/etc/keys/xxx-firebase-admin-sdk.json can not be read: SplFileObject::__construct(/docker/home/centerx/etc/keys/xxx-firebase-admin-sdk.json): ...
PHP Warning:  openssl_sign(): Supplied key param cannot be coerced into a private key in /docker/home/centerx/vendor/firebase/php-jwt/src/JWT.php on line 209
Warning: openssl_sign(): Supplied key param cannot be coerced into a private key in /docker/home/centerx/vendor/firebase/php-jwt/src/JWT.php on line 209

d(): Array
(
[topic1621427903] => OpenSSL unable to sign data
)

대 용량 사진/파일 업로드

에러 메시지) NOTICE: PHP message: PHP Warning: POST Content-Length of 81320805 bytes exceeds the limit of 67108864 bytes in Unknown on line 0

해결책)


php.ini)
upload_max_filesize = 1000M;
post_max_size = 1000M;

nginx.conf)
client_max_body_size    500M;

광고 기능

팁, 트릭

Known Issues

404 error on push token renew